From 312c704ba65d2b2d447ba332fc948e301e5a34c5 Mon Sep 17 00:00:00 2001 From: Qing Tao <55564570+VickyAtAZ@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:39:50 -0700 Subject: [PATCH 1/2] ATOM-15352 Find a solution to modify a render pipeline when enable a feature gem (#960) * Added atom_rpi_tools python module in Atom_RPI gem. The tool includes functions to modify pass template data and some utility functions. * Added pytest tests for atom_rpi_tools --- Gems/Atom/RPI/CMakeLists.txt | 2 + Gems/Atom/RPI/Tools/CMakeLists.txt | 23 ++ Gems/Atom/RPI/Tools/README.txt | 39 +++ Gems/Atom/RPI/Tools/__init__.py | 10 + .../RPI/Tools/atom_rpi_tools/pass_data.py | 210 ++++++++++++ .../Tools/atom_rpi_tools/tests/__init__.py | 10 + .../tests/test_pass_template.py | 303 ++++++++++++++++++ .../Tools/atom_rpi_tools/tests/test_utils.py | 53 +++ .../tests/testdata/pass_requests.json | 116 +++++++ .../tests/testdata/pass_slots.json | 21 ++ .../tests/testdata/pass_test_bad.json | 6 + Gems/Atom/RPI/Tools/atom_rpi_tools/utils.py | 31 ++ Gems/Atom/RPI/Tools/setup.py | 33 ++ 13 files changed, 857 insertions(+) create mode 100644 Gems/Atom/RPI/Tools/CMakeLists.txt create mode 100644 Gems/Atom/RPI/Tools/README.txt create mode 100644 Gems/Atom/RPI/Tools/__init__.py create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/pass_data.py create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/tests/__init__.py create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_pass_template.py create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_utils.py create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_requests.json create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_slots.json create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_test_bad.json create mode 100644 Gems/Atom/RPI/Tools/atom_rpi_tools/utils.py create mode 100644 Gems/Atom/RPI/Tools/setup.py diff --git a/Gems/Atom/RPI/CMakeLists.txt b/Gems/Atom/RPI/CMakeLists.txt index 20a680bce9..8cea783633 100644 --- a/Gems/Atom/RPI/CMakeLists.txt +++ b/Gems/Atom/RPI/CMakeLists.txt @@ -10,3 +10,5 @@ # add_subdirectory(Code) +add_subdirectory(Tools) + diff --git a/Gems/Atom/RPI/Tools/CMakeLists.txt b/Gems/Atom/RPI/Tools/CMakeLists.txt new file mode 100644 index 0000000000..35df664ab2 --- /dev/null +++ b/Gems/Atom/RPI/Tools/CMakeLists.txt @@ -0,0 +1,23 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +if (PAL_TRAIT_BUILD_HOST_TOOLS) + ly_pip_install_local_package_editable(${CMAKE_CURRENT_LIST_DIR} atom_rpi_tools) + + if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_pytest( + NAME RPI::atom_rpi_tools_tests + PATH ${CMAKE_CURRENT_LIST_DIR}/atom_rpi_tools/tests/ + TIMEOUT 30 + ) + endif() +endif() + diff --git a/Gems/Atom/RPI/Tools/README.txt b/Gems/Atom/RPI/Tools/README.txt new file mode 100644 index 0000000000..ee2e9264da --- /dev/null +++ b/Gems/Atom/RPI/Tools/README.txt @@ -0,0 +1,39 @@ +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + + +INTRODUCTION +------------ + +atom_rpi_tools is a Python project that contains a collection of tools +developed by the Atom team. The project contains the following tools: + + * Render pipeline merge tool: + A library to manipulate .pass asset files and help gems create scripts to update render pipeline + + +REQUIREMENTS +------------ + + * Python 3.7.5 (64-bit) + +It is recommended that you completely remove any other versions of Python +installed on your system. + + +INSTALL +----------- +It is recommended to set up these these tools with Lumberyard's CMake build commands. + + +UNINSTALLATION +-------------- + +The preferred way to uninstall the project is: +(engine install root)/python/python -m pip uninstall atom_rpi_tools diff --git a/Gems/Atom/RPI/Tools/__init__.py b/Gems/Atom/RPI/Tools/__init__.py new file mode 100644 index 0000000000..79f8fa4422 --- /dev/null +++ b/Gems/Atom/RPI/Tools/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/pass_data.py b/Gems/Atom/RPI/Tools/atom_rpi_tools/pass_data.py new file mode 100644 index 0000000000..e5e7a839bd --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/pass_data.py @@ -0,0 +1,210 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +""" + +import sys, os +import json +import shutil + +class PassTemplate: + # This class provide necessary functions for insert pass requests and update connections + # which are common functions required for adding features. + # It doesn't include the remove/delete furnctions since that's not common case for merging render pipeline + def __init__(self, filePath: str): + self.initialized = False + self.file_path: str = filePath + #load the json file + json_data = open(filePath, "r") + self.file_data = json.load(json_data) + + if 'ClassName' not in self.file_data or 'ClassData' not in self.file_data or self.file_data['ClassName']!='PassAsset' or 'PassTemplate' not in self.file_data['ClassData']: + raise KeyError('the json file is not a PassAsset file') + return + + if 'PassRequests' in self.file_data['ClassData']['PassTemplate']: + self.passRequests = self.file_data['ClassData']['PassTemplate']['PassRequests'] + + if 'Slots' in self.file_data['ClassData']['PassTemplate']: + self.slots = self.file_data['ClassData']['PassTemplate']['Slots'] + + self.initialized = True + print('PassTemplate is loaded from ', filePath) + + def find_pass(self, passName): + # return pass's index in PassRequests if a PassRequest with input passName exists + if not hasattr(self, 'passRequests'): + return -1 + index = 0 + for passRequest in self.passRequests: + if passRequest['Name'] == passName: + return index + index += 1 + return -1 + + def get_pass_count(self): + if not hasattr(self, 'passRequests'): + return 0 + return len(self.passRequests) + + def __validate_pass_request_data(self, passRequest): + if ('Name' not in passRequest or 'TemplateName' not in passRequest): + raise KeyError('invalid pass request data') + + def __ensure_pass_requests_key(self): + if not hasattr(self, 'passRequests'): + self.file_data['ClassData']['PassTemplate']['PassRequests'] = [] + self.passRequests = self.file_data['ClassData']['PassTemplate']['PassRequests'] + + def __ensure_pass_slots_key(self): + if not hasattr(self, 'slots'): + self.file_data['ClassData']['PassTemplate']['Slots'] = [] + self.slots = self.file_data['ClassData']['PassTemplate']['Slots'] + + def insert_pass_request(self, location, passRequest): + self.__validate_pass_request_data(passRequest) + + if (self.find_pass(passRequest['Name']) >= 0): + raise ValueError('pass request ', passRequest['Name'], ' is already exist') + # insert a passRequest before the specified location + self.__ensure_pass_requests_key() + self.passRequests.insert(location, passRequest) + + def replace_references_after(self, startPassRequest, oldPass, oldSlot, newPass, newSlot): + if not hasattr(self, 'passRequests'): + return 0 + # from all pass requests after startPassRequest + # replace all attachment references which uses oldPass and oldSlot + # with newPass and newSlot + started = False + replaced_count = 0 + for request in self.passRequests: + if started: + if ('Connections' in request): + for connection in request['Connections']: + if connection['AttachmentRef']['Pass'] == oldPass and connection['AttachmentRef']['Attachment'] == oldSlot: + connection['AttachmentRef']['Pass'] = newPass + connection['AttachmentRef']['Attachment'] = newSlot + replaced_count += 1 + if request['Name'] == startPassRequest and not started: + started = True + return replaced_count + + def replace_references_for(self, passRequest, oldPass, oldSlot, newPass, newSlot): + if not hasattr(self, 'passRequests'): + return 0 + #replace pass reference for the specified passRequest + replaced_count = 0 + for request in self.passRequests: + if request['Name'] == passRequest: + if ('Connections' in request): + for connection in request['Connections']: + if connection['AttachmentRef']['Pass'] == oldPass and connection['AttachmentRef']['Attachment'] == oldSlot: + connection['AttachmentRef']['Pass'] = newPass + connection['AttachmentRef']['Attachment'] = newSlot + replaced_count += 1 + return replaced_count #return when the specified pass request is updated. + return replaced_count + + def __validate_slot_data(self, slotData): + if ('Name' not in slotData or 'SlotType' not in slotData): + raise KeyError('invalid slot data') + + def get_slot_count(self): + if not hasattr(self, 'slots'): + return 0 + return len(self.slots) + + def find_slot(self, slotName): + # return slot's index in Slots if a PassRequest with input passName exists + if not hasattr(self, 'slots'): + return -1 + index = 0 + for slot in self.slots: + if slot['Name'] == slotName: + return index + index += 1 + return -1 + + def insert_slot(self, location, newSlotData): + # insert a new slot at specified location + self.__validate_slot_data(newSlotData) + # check if the slot already exist + if (self.find_slot(newSlotData['Name']) >= 0): + raise ValueError('Slot ', newSlotData['Name'], ' is already exist') + + self.__ensure_pass_slots_key() + self.slots.insert(location, newSlotData) + + def add_slot(self, newSlotData): + # append a new slot to slots + self.__validate_slot_data(newSlotData) + # check if the slot already exist + if (self.find_slot(newSlotData['Name']) >= 0): + raise ValueError('Slot ', newSlotData['Name'], ' is already exist') + + self.__ensure_pass_slots_key() + self.slots.append(newSlotData) + + def get_pass_request(self, passName): + if not hasattr(self, 'passRequests'): + return + # Get the pass request from PassRequests with matching pass name + for passRequest in self.passRequests: + if passRequest['Name'] == passName: + return passRequest + + def save(self): + # backup the original file + backupFilePath = self.file_path +'.backup' + shutil.copyfile(self.file_path, backupFilePath) + # save and overwrite file + with open(self.file_path, 'w') as json_file: + json.dump(self.file_data, json_file, indent = 4) + print('File [', self.file_path, '] is updated. Old version is saved in [', backupFilePath, ']') + + +class PassRequest: + + def __init__(self, passRequest: object): + self.pass_request = passRequest + if 'Connections' in passRequest: + self.connections = passRequest['Connections'] + + def __validate_connection(self, connection): + if ('LocalSlot' not in connection or 'AttachmentRef' not in connection): + raise KeyError('invalid connection data') + + def __ensure_connections_key(self): + if not hasattr(self, 'connections'): + self.pass_request['Connections'] = [] + self.connections = self.pass_request['Connections'] + + def get_connection_count(self): + if not hasattr(self, 'connections'): + return 0 + return len(self.connections) + + def find_connection(self, localSlotName): + if not hasattr(self, 'connections'): + return -1 + index = 0 + for connection in self.connections: + if connection['LocalSlot'] == localSlotName: + return index + index += 1 + return -1 + + def add_connection(self, newConnection): + self.__validate_connection(newConnection) + if self.find_connection(newConnection['LocalSlot']) >= 0: + raise ValueError('connection ', newConnection['LocalSlot'], ' already exists') + self.__ensure_connections_key() + self.connections.append(newConnection) \ No newline at end of file diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/__init__.py b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/__init__.py new file mode 100644 index 0000000000..79f8fa4422 --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_pass_template.py b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_pass_template.py new file mode 100644 index 0000000000..d8cfc4840a --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_pass_template.py @@ -0,0 +1,303 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +Unit tests for pass_data.py +""" +import os +import pytest +import shutil +import json +from atom_rpi_tools.pass_data import PassTemplate +from atom_rpi_tools.pass_data import PassRequest + +good_pass_requests_file = os.path.join(os.path.dirname(__file__), 'testdata/pass_requests.json') +good_pass_slots_file = os.path.join(os.path.dirname(__file__), 'testdata/pass_slots.json') +bad_test_data_file = os.path.join(os.path.dirname(__file__), 'testdata/pass_test_bad.json') + +@pytest.fixture +def pass_requests_template(tmpdir): + filename = 'pass_requests.json' + source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename) + destFilePath = os.path.join(tmpdir, 'pass_requests.json') + shutil.copyfile(source_path, destFilePath) + return PassTemplate(destFilePath) + +@pytest.fixture +def pass_slots_template(tmpdir): + filename = 'pass_slots.json' + source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename) + destFilePath = os.path.join(tmpdir, 'pass_requests.json') + shutil.copyfile(source_path, destFilePath) + return PassTemplate(destFilePath) + +@pytest.fixture +def new_pass_request(): + pass_request = json.loads('{\"Name\": \"InsertPass\",\"TemplateName\": \"InsertPassTemplate\"}') + return pass_request + +@pytest.fixture +def new_slot(): + slot = json.loads('{\"Name\": \"NewSlot\",\"SlotType\": \"Input\"}') + return slot + +@pytest.fixture +def new_connection(): + connection = json.loads('{\"LocalSlot\": \"color\", \"AttachmentRef\": { \"Pass\": \"Parent\", \"Attachment\": \"DepthStencil\"}}') + return connection + +def test_PassTemplate_Initialize_BadPassTemplateData_ExceptionThrown(): + with pytest.raises(KeyError): + PassTemplate(bad_test_data_file) + +def test_PassTemplate_FindPass_Success(pass_requests_template): + assert pass_requests_template.find_pass('OpaquePass') == 0 + assert pass_requests_template.find_pass('ImGuiPass') == 4 + assert pass_requests_template.find_pass('NotExistPass') == -1 + +def test_PassTemplate_InsertPassRequest_AtBegining_Success(pass_requests_template, new_pass_request): + template = pass_requests_template + pass_count = template.get_pass_count() + template.insert_pass_request(0, new_pass_request) + assert template.find_pass(new_pass_request['Name']) == 0 + assert template.get_pass_count() == pass_count+1 + + # verify the change is saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.find_pass(new_pass_request['Name'])== 0 + assert saved_tamplate.get_pass_count() == pass_count+1 + +def test_PassTemplate_InsertPassRequest_AtEnd_Success(pass_requests_template, new_pass_request): + template = pass_requests_template + pass_count = template.get_pass_count() + template.insert_pass_request(pass_count, new_pass_request) + assert template.find_pass(new_pass_request['Name']) == pass_count + assert template.get_pass_count() == pass_count+1 + + # verify the change is saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.find_pass(new_pass_request['Name']) == pass_count + assert saved_tamplate.get_pass_count() == pass_count+1 + +def test_PassTemplate_InsertPassRequest_WithDuplicatedName_ExceptionThrown(pass_requests_template, new_pass_request): + template = pass_requests_template + # insert new pass request + template.insert_pass_request(0, new_pass_request) + pass_count = template.get_pass_count() + # exception when insert the same pass again + with pytest.raises(ValueError): + template.insert_pass_request(2, new_pass_request) + # pass count doesn't change + assert template.get_pass_count() == pass_count + +def test_PassTemplate_InsertPassRequest_WithBadData_ExceptionThrown(pass_requests_template): + template = pass_requests_template + pass_count = template.get_pass_count() + bad_pass_request = json.loads('{\"name\":\"value\"}') + with pytest.raises(KeyError): + template.insert_pass_request(2, bad_pass_request) + assert template.get_pass_count() == pass_count + +def test_PassTemplate_InsertPassRequest_AtOutOfRange_AppendSuccess(pass_requests_template, new_pass_request): + template = pass_requests_template + pass_count = template.get_pass_count() + template.insert_pass_request(pass_count+2, new_pass_request) + assert template.find_pass(new_pass_request['Name']) == pass_count + assert template.get_pass_count() == pass_count+1 + +def test_PassTemplate_ReplaceReferencesAfter_Success(pass_requests_template): + # replace OpaquePass.DepthStencil with Parent.DepthStencil' + refPass = 'OpaquePass' + # there are 2 passes after OpaquePass which use OpaquePass.DepthStencil as attachment reference + assert pass_requests_template.replace_references_after(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 2 + # after the previous replacement, there it no OpaquePass.DepthStencil reference + refPass = 'TransparentPass' + assert pass_requests_template.replace_references_after(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0 + + # verify changes are saved + pass_requests_template.save() + saved_tamplate = PassTemplate(pass_requests_template.file_path) + assert saved_tamplate.replace_references_after('OpaquePass', 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0 + +def test_PassTemplate_ReplaceReferencesFor_Success(pass_requests_template): + refPass = 'TransparentPass' + assert pass_requests_template.replace_references_for(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 1 + refPass = '2DPass' + assert pass_requests_template.replace_references_for(refPass, 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0 + + # verify changes are saved + pass_requests_template.save() + saved_tamplate = PassTemplate(pass_requests_template.file_path) + # no reference of OpaquePass.DepthStencil in TransparentPass + assert saved_tamplate.replace_references_for('TransparentPass', 'OpaquePass', 'DepthStencil', 'Parent', 'DepthStencil') == 0 + +def test_PassTemplate_FindSlot_Success(pass_slots_template): + assert pass_slots_template.find_slot('Color') == -1 + assert pass_slots_template.find_slot('DepthStencil') == 0 + assert pass_slots_template.find_slot('ColorInputOutput') == 1 + +def test_PassTemplate_InsertSlot_AtBegining_Success(pass_slots_template, new_slot): + template = pass_slots_template + slot_count = template.get_slot_count() + depth_stencil_slot = template.find_slot('DepthStencil') + template.insert_slot(0, new_slot) + assert template.find_slot(new_slot['Name']) == 0 + assert template.find_slot('DepthStencil') == depth_stencil_slot+1 # DepthStencil moved back by 1 + assert template.get_slot_count() == slot_count+1 + + # verify the change is saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.find_slot(new_slot['Name']) == 0 + assert saved_tamplate.get_slot_count() == slot_count+1 + +def test_PassTemplate_InsertSlot_AtEnd_Success(pass_slots_template, new_slot): + template = pass_slots_template + slot_count = template.get_slot_count() + depth_stencil_slot = template.find_slot('DepthStencil') + template.insert_slot(slot_count, new_slot) + assert template.find_slot(new_slot['Name']) == slot_count + assert template.find_slot('DepthStencil') == depth_stencil_slot + assert template.get_slot_count() == slot_count+1 + + # verify the change is saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.find_slot(new_slot['Name']) == slot_count + assert saved_tamplate.get_slot_count() == slot_count+1 + +def test_PassTemplate_AddSlot_GoodSlotData_Success(pass_slots_template, new_slot): + template = pass_slots_template + slot_count = template.get_slot_count() + depth_stencil_slot = template.find_slot('DepthStencil') + template.add_slot(new_slot) + assert template.find_slot(new_slot['Name']) == slot_count + assert template.find_slot('DepthStencil') == depth_stencil_slot + assert template.get_slot_count() == slot_count+1 + + # verify the change is saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.find_slot(new_slot['Name']) == slot_count + assert saved_tamplate.get_slot_count() == slot_count+1 + +def test_PassTemplate_InsertSlot_OutOfRange_AppendSuccess(pass_slots_template, new_slot): + template = pass_slots_template + slot_count = template.get_slot_count() + template.insert_slot(slot_count+3, new_slot) + assert template.find_slot(new_slot['Name']) == slot_count + assert template.get_slot_count() == slot_count+1 + +def test_PassTemplate_AddDuplicateSlot_ExceptionThrown(pass_slots_template, new_slot): + template = pass_slots_template + slot_count = template.get_slot_count() + template.add_slot(new_slot) + + with pytest.raises(ValueError): + template.insert_slot(0, new_slot) + with pytest.raises(ValueError): + template.add_slot(new_slot) + +def test_PassTemplate_InsertOrAddSlot_WithBadSlotData_ExceptionThrown(pass_slots_template): + template = pass_slots_template + slot_count = template.get_slot_count() + bad_slot = json.loads('{\"slot\": \"xxx\"}') + + with pytest.raises(KeyError): + template.insert_slot(0, bad_slot) + with pytest.raises(KeyError): + template.add_slot(bad_slot) + +def test_PassReqeuest_Initialize_WithExistPassReqeuestFromPassTemplate_Success(pass_requests_template): + template = pass_requests_template + request = PassRequest(template.get_pass_request('OpaquePass')) + connection_count = request.get_connection_count() + assert connection_count == 2 + +def test_PassTemplate_GetPassRequest_NotExist_ReturnNull(pass_requests_template): + assert not pass_requests_template.get_pass_request('NotExistPass') + +def test_PassReqeuest_AddConnection_WithExistingConnections_Success(pass_requests_template, new_connection): + template = pass_requests_template + request = PassRequest(template.get_pass_request('OpaquePass')) + connection_count = request.get_connection_count() + request.add_connection(new_connection) + connection_count += 1 + assert request.get_connection_count() == connection_count + + # verify changes are saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + saved_request = PassRequest(saved_tamplate.get_pass_request('OpaquePass')) + assert saved_request.get_connection_count() == connection_count + +def test_PassReqeuest_AddConnection_WithNoExistingConnections_Success(pass_requests_template, new_connection): + template = pass_requests_template + request = PassRequest(template.get_pass_request('ImGuiPass')) + assert request.get_connection_count() == 0 + request.add_connection(new_connection) + assert request.get_connection_count() == 1 + + # verify changes are saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + saved_request = PassRequest(saved_tamplate.get_pass_request('ImGuiPass')) + assert saved_request.get_connection_count() == 1 + +def test_PassReqeuest_AddConnection_WithDuplicatedName_ExceptionThrown(pass_requests_template, new_connection): + template = pass_requests_template + request = PassRequest(template.get_pass_request('OpaquePass')) + request.add_connection(new_connection) + with pytest.raises(ValueError): + request.add_connection(new_connection) + +def test_PassReqeuest_AddConnect_BadConnectionData_ExceptionThrown(pass_requests_template, new_connection): + template = pass_requests_template + request = PassRequest(template.get_pass_request('OpaquePass')) + bad_connection = json.loads('{\"xxx\": \"xxx\"}') + with pytest.raises(KeyError): + request.add_connection(bad_connection) + +def test_PassTemplate_InsertSlot_ToEmptyList_Success(pass_requests_template, new_slot): + template = pass_requests_template + # test insert slot function to pass template which doesn't have any slots + slot_count = template.get_slot_count() + assert slot_count == 0 + + assert template.find_slot(new_slot['Name'])==-1 + pass_requests_template.insert_slot(0, new_slot) + assert template.find_slot(new_slot['Name']) == 0 + assert template.get_slot_count() == 1 + + # verify changes are saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.get_slot_count() == 1 + +def test_PassTempalte_InsertPassRequest_ToEmptyList_Success(pass_slots_template, new_pass_request): + template = pass_slots_template + # test insert pass function to pass template which doesn't have any pass requests + pass_count = template.get_pass_count() + assert pass_count == 0 + template.insert_pass_request(0, new_pass_request) + assert template.find_pass(new_pass_request['Name']) == 0 + assert template.get_pass_count() == 1 + + # verify changes are saved + template.save() + saved_tamplate = PassTemplate(template.file_path) + assert saved_tamplate.get_pass_count() == 1 + +def test_PassTemplate_Save_Success(pass_requests_template): + pass_requests_template.save() + saved_tamplate = PassTemplate(pass_requests_template.file_path) + assert os.path.exists(pass_requests_template.file_path) + assert os.path.exists(pass_requests_template.file_path +'.backup') diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_utils.py b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_utils.py new file mode 100644 index 0000000000..ac1bb6edf9 --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/test_utils.py @@ -0,0 +1,53 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +Unit tests for utils.py +""" +import pytest +import os +import atom_rpi_tools.utils as utils + + +def test_FindOrCopyFile_DestFileNotExist_CopySuccess(tmpdir): + # created dir and copied + filename = 'pass_requests.json' + source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename) + dest_path = os.path.join(tmpdir, 'testdata/', 'pass_requests.json') + assert not os.path.exists(dest_path) + utils.find_or_copy_file(dest_path, source_path) + assert os.path.exists(dest_path) + source_size = os.path.getsize(source_path) + dest_size = os.path.getsize(dest_path) + assert source_size == dest_size + +def test_FindOrCopyFile_DestFileAlreadyExists_Skip(tmpdir): + # copy %cur_dir%/testdata/pass_requests.json to tempdir/testdata/pass_requests.json + filename = 'pass_requests.json' + source_path = os.path.join(os.path.dirname(__file__), 'testdata/', filename) + dest_path = os.path.join(tmpdir, 'testdata/', 'pass_requests.json') + utils.find_or_copy_file(dest_path, source_path) + + # skip if dest_path already exists + assert os.path.exists(dest_path) + before_size = os.path.getsize(dest_path) + source_path = os.path.join(os.path.dirname(__file__), 'testdata/', 'pass_slots.json') + before_source_size = os.path.getsize(source_path) + assert before_size != source_path + utils.find_or_copy_file(dest_path, source_path) + after_size = os.path.getsize(dest_path) + assert before_size == after_size + + +def test_FindOrCopyFile_SourceFileNotExists_ExceptionThrown(tmpdir): + # report error if source doesn't exist + bad_source_path = 'notexist.dat' + dest_path = os.path.join(tmpdir, 'notexist.dat') + with pytest.raises(ValueError): + utils.find_or_copy_file(dest_path, bad_source_path) diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_requests.json b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_requests.json new file mode 100644 index 0000000000..f3dd3f7a19 --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_requests.json @@ -0,0 +1,116 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "PipelineTemplate", + "PassClass": "ParentPass", + "PassRequests": [ + { + "Name": "OpaquePass", + "TemplateName": "OpaquePassTemplate", + "Connections": [ + { + "LocalSlot": "DepthStencil", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DepthStencil" + } + }, + { + "LocalSlot": "ColorInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ColorInputOutput" + } + } + ] + }, + { + "Name": "TransparentPass", + "TemplateName": "TransparentPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "DepthStencil", + "AttachmentRef": { + "Pass": "OpaquePass", + "Attachment": "DepthStencil" + } + }, + { + "LocalSlot": "ColorInputOutput", + "AttachmentRef": { + "Pass": "OpaquePass", + "Attachment": "Color" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "transparent", + "DrawListSortType": "KeyThenReverseDepth", + "PipelineViewTag": "MainCamera", + "PassSrgAsset": { + "FilePath": "shaderlib/atom/features/pbr/transparentpasssrg.azsli:PassSrg" + } + } + }, + { + "Name": "AuxGeomPass", + "TemplateName": "AuxGeomPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "DepthStencil", + "AttachmentRef": { + "Pass": "OpaquePass", + "Attachment": "DepthStencil" + } + }, + { + "LocalSlot": "ColorInputOutput", + "AttachmentRef": { + "Pass": "TransparentPass", + "Attachment": "ColorInputOutput" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "auxgeom", + "PipelineViewTag": "MainCamera" + } + }, + { + "Name": "2DPass", + "TemplateName": "UIPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "ColorInputOutput", + "AttachmentRef": { + "Pass": "TransparentPass", + "Attachment": "ColorInputOutput" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "2dpass", + "PipelineViewTag": "MainCamera" + } + }, + { + "Name": "ImGuiPass", + "TemplateName": "ImGuiPassTemplate", + "PassData": { + "$type": "ImGuiPassData", + "IsDefaultImGui": true + } + } + ] + } + } +} diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_slots.json b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_slots.json new file mode 100644 index 0000000000..7274b0968a --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_slots.json @@ -0,0 +1,21 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "PipelineTemplate", + "PassClass": "ParentPass", + "Slots": [ + { + "Name": "DepthStencil", + "SlotType": "InputOutput" + }, + { + "Name": "ColorInputOutput", + "SlotType": "InputOutput" + } + ] + } + } +} diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_test_bad.json b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_test_bad.json new file mode 100644 index 0000000000..a0e5ffcc90 --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/tests/testdata/pass_test_bad.json @@ -0,0 +1,6 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassData": { + } +} \ No newline at end of file diff --git a/Gems/Atom/RPI/Tools/atom_rpi_tools/utils.py b/Gems/Atom/RPI/Tools/atom_rpi_tools/utils.py new file mode 100644 index 0000000000..90037a8287 --- /dev/null +++ b/Gems/Atom/RPI/Tools/atom_rpi_tools/utils.py @@ -0,0 +1,31 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import os.path +from os import path +import shutil +import json + + +def find_or_copy_file(destFilePath, sourceFilePath): + if path.exists(destFilePath): + return + if not path.exists(sourceFilePath): + raise ValueError('find_or_copy_file: source file [', sourceFilePath, '] doesn\'t exist') + return + + dstDir = path.dirname(destFilePath) + if not path.isdir(dstDir): + os.makedirs(dstDir) + shutil.copyfile(sourceFilePath, destFilePath) + +def load_json_file(filePath): + file_stream = open(filePath, "r") + return json.load(file_stream) diff --git a/Gems/Atom/RPI/Tools/setup.py b/Gems/Atom/RPI/Tools/setup.py new file mode 100644 index 0000000000..ab494ec773 --- /dev/null +++ b/Gems/Atom/RPI/Tools/setup.py @@ -0,0 +1,33 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import os +import platform + +from setuptools import setup, find_packages + +PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) + +PYTHON_64 = platform.architecture()[0] == '64bit' + +if __name__ == '__main__': + if not PYTHON_64: + raise RuntimeError("32-bit Python is not a supported platform.") + + with open(os.path.join(PROJECT_ROOT, 'README.txt')) as f: + long_description = f.read() + + setup( + name="atom_rpi_tools", + version="1.0.0", + description='Python interface to Atom RPI tools', + long_description=long_description, + packages=find_packages(exclude=['tests']) + ) From bb92e4c0b86963d59916bbfc844d5d998215ae1f Mon Sep 17 00:00:00 2001 From: rgba16f <82187279+rgba16f@users.noreply.github.com> Date: Wed, 2 Jun 2021 19:49:38 -0500 Subject: [PATCH 2/2] Font update to dynamic draw per view (#1085) * Move AtomFont over to use the new per viewport dynamic draw context. Remove scene tracking and listening for bootstrap scene created. Remove build dependency on the Bootstrap gem. Add build dependency on the AtomBridge gem. FFont's are now initialized with a viewport Id. Remove previous DynamicDraw context per scene system. Verify FFont can get a dynamic draw context before attempting initialization. Ensure a render scene exists before attempting font initialization (as a proxy for rendering has begun) * Move AtomFont FFont to use ShaderInputNameIndex's This allowed removing all of the InitFont function as no longer need to query compiled shader info for constant data offsets. * cache the AZ::Name used to find the dynamic draw context rather than recreate it each use --- .../AtomFont/Code/CMakeLists.txt | 3 +- .../AtomLyIntegration/AtomFont/AtomFont.h | 15 +-- .../AtomLyIntegration/AtomFont/FFont.h | 22 +--- .../AtomFont/Code/Source/AtomFont.cpp | 74 +++--------- .../AtomFont/Code/Source/FFont.cpp | 105 ++++-------------- 5 files changed, 51 insertions(+), 168 deletions(-) diff --git a/Gems/AtomLyIntegration/AtomFont/Code/CMakeLists.txt b/Gems/AtomLyIntegration/AtomFont/Code/CMakeLists.txt index d3b8c8cde7..0045b37687 100644 --- a/Gems/AtomLyIntegration/AtomFont/Code/CMakeLists.txt +++ b/Gems/AtomLyIntegration/AtomFont/Code/CMakeLists.txt @@ -29,7 +29,8 @@ ly_add_target( Legacy::CryCommon Gem::Atom_RHI.Reflect Gem::Atom_RPI.Public - Gem::Atom_Bootstrap.Headers + PUBLIC + Gem::Atom_AtomBridge.Static ) ################################################################################ diff --git a/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/AtomFont.h b/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/AtomFont.h index d5463fc15c..d21796cf17 100644 --- a/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/AtomFont.h +++ b/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/AtomFont.h @@ -27,11 +27,14 @@ #include #include +#include namespace AZ { class FFont; + static constexpr char AtomFontDynamicDrawContextName[] = "AtomFont"; + //! AtomFont is the font system manager. //! AtomFont manages the lifetime of FFont instances, each of which represents an individual font (e.g Courier New Italic) @@ -90,13 +93,6 @@ namespace AZ AzFramework::FontDrawInterface* GetFontDrawInterface(AzFramework::FontId fontId) const override; AzFramework::FontDrawInterface* GetDefaultFontDrawInterface() const override; - void SceneAboutToBeRemoved(AzFramework::Scene& scene); - - - // Atom DynamicDraw interface management - AZ::RHI::Ptr GetOrCreateDynamicDrawForScene(AZ::RPI::Scene* scene); - - public: void UnregisterFont(const char* fontName); @@ -108,8 +104,6 @@ namespace AZ using FontFamilyMap = AZStd::unordered_map>; using FontFamilyReverseLookupMap = AZStd::unordered_map; - using SceneToDynamicDrawMap = AZStd::unordered_map>; - private: //! Convenience method for loading fonts IFFont* LoadFont(const char* fontName); @@ -145,9 +139,6 @@ namespace AZ int r_persistFontFamilies = 1; //!< Persist fonts for application lifetime to prevent unnecessary work; enabled by default. AZStd::vector m_persistedFontFamilies; //!< Stores persisted fonts (if "persist font families" is enabled) - - SceneToDynamicDrawMap m_sceneToDynamicDrawMap; - AZStd::shared_mutex m_sceneToDynamicDrawMutex; }; } #endif diff --git a/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/FFont.h b/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/FFont.h index 1e224d6090..78d74a784a 100644 --- a/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/FFont.h +++ b/Gems/AtomLyIntegration/AtomFont/Code/Include/AtomLyIntegration/AtomFont/FFont.h @@ -42,11 +42,9 @@ #include #include #include +#include #include -#include -#include - struct ISystem; namespace AZ @@ -68,7 +66,6 @@ namespace AZ : public IFFont , public AZStd::intrusive_refcount , public AzFramework::FontDrawInterface - , private AZ::Render::Bootstrap::NotificationBus::Handler { using ref_count = AZStd::intrusive_refcount; friend FontDeleter; @@ -168,8 +165,8 @@ namespace AZ struct FontShaderData { - AZ::RHI::ShaderInputImageIndex m_imageInputIndex; - AZ::RHI::ShaderInputConstantIndex m_viewProjInputIndex; + AZ::RHI::ShaderInputNameIndex m_imageInputIndex = "m_texture"; + AZ::RHI::ShaderInputNameIndex m_viewProjInputIndex = "m_worldToProj"; }; public: @@ -230,7 +227,6 @@ namespace AZ private: virtual ~FFont(); - bool InitFont(AZ::RPI::Scene* renderScene); bool InitTexture(); bool InitCache(); @@ -281,8 +277,6 @@ namespace AZ void ScaleCoord(const RHI::Viewport& viewport, float& x, float& y) const; - void OnBootstrapSceneReady(AZ::RPI::Scene* bootstrapScene) override; - RPI::WindowContextSharedPtr GetDefaultWindowContext() const; RPI::ViewportContextPtr GetDefaultViewportContext() const; @@ -303,6 +297,8 @@ namespace AZ string m_name; string m_curPath; + AZ::Name m_dynamicDrawContextName = AZ::Name(AZ::AtomFontDynamicDrawContextName); + FontTexture* m_fontTexture = nullptr; size_t m_fontBufferSize = 0; @@ -315,13 +311,6 @@ namespace AZ AtomFont* m_atomFont = nullptr; bool m_fontTexDirty = false; - enum class InitializationState : AZ::u8 - { - Uninitialized, - Initializing, - Initialized - }; - AZStd::atomic m_fontInitializationState = InitializationState::Uninitialized; FontEffects m_effects; @@ -356,6 +345,7 @@ namespace AZ if (font && font->m_atomFont) { font->m_atomFont->UnregisterFont(font->m_name); + font->m_atomFont = nullptr; } delete font; diff --git a/Gems/AtomLyIntegration/AtomFont/Code/Source/AtomFont.cpp b/Gems/AtomLyIntegration/AtomFont/Code/Source/AtomFont.cpp index 623a931ac9..fd8a4f26ed 100644 --- a/Gems/AtomLyIntegration/AtomFont/Code/Source/AtomFont.cpp +++ b/Gems/AtomLyIntegration/AtomFont/Code/Source/AtomFont.cpp @@ -354,17 +354,26 @@ AZ::AtomFont::AtomFont(ISystem* system) #endif AZ::Interface::Register(this); - m_sceneEventHandler = AzFramework::ISceneSystem::SceneEvent::Handler( - [this](AzFramework::ISceneSystem::EventType eventType, const AZStd::shared_ptr& scene) + // register font per viewport dynamic draw context. + static const char* shaderFilepath = "Shaders/SimpleTextured.azshader"; + AZ::AtomBridge::PerViewportDynamicDraw::Get()->RegisterDynamicDrawContext( + AZ::Name(AZ::AtomFontDynamicDrawContextName), + [](RPI::Ptr drawContext) { - if (eventType == AzFramework::ISceneSystem::EventType::ScenePendingRemoval) - { - SceneAboutToBeRemoved(*scene); - } + Data::Instance shader = AZ::RPI::LoadShader(shaderFilepath); + AZ::RPI::ShaderOptionList shaderOptions; + shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_useColorChannels"), AZ::Name("false"))); + shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_clamp"), AZ::Name("true"))); + drawContext->InitShaderWithVariant(shader, &shaderOptions); + drawContext->InitVertexFormat( + { + {"POSITION", RHI::Format::R32G32B32_FLOAT}, + {"COLOR", RHI::Format::B8G8R8A8_UNORM}, + {"TEXCOORD0", RHI::Format::R32G32_FLOAT} + }); + drawContext->EndInit(); }); - auto sceneSystem = AzFramework::SceneSystemInterface::Get(); - AZ_Assert(sceneSystem, "Font created before the scene system is available."); - sceneSystem->ConnectToEvents(m_sceneEventHandler); + } AZ::AtomFont::~AtomFont() @@ -860,52 +869,5 @@ XmlNodeRef AZ::AtomFont::LoadFontFamilyXml(const char* fontFamilyName, string& o return root; } - -void AZ::AtomFont::SceneAboutToBeRemoved(AzFramework::Scene& scene) -{ - AZ::RPI::ScenePtr* rpiScene = scene.FindSubsystem(); - if (rpiScene) - { - AZStd::lock_guard lock(m_sceneToDynamicDrawMutex); - if (auto it = m_sceneToDynamicDrawMap.find(rpiScene->get()); it != m_sceneToDynamicDrawMap.end()) - { - m_sceneToDynamicDrawMap.erase(it); - } - } -} - -AZ::RHI::Ptr AZ::AtomFont::GetOrCreateDynamicDrawForScene(AZ::RPI::Scene* scene) -{ - static const char* shaderFilepath = "Shaders/SimpleTextured.azshader"; - - { - // shared lock while reading - AZStd::shared_lock lock(m_sceneToDynamicDrawMutex); - - if (auto it = m_sceneToDynamicDrawMap.find(scene); it != m_sceneToDynamicDrawMap.end()) - { - return it->second; - } - } - - // Create and initialize DynamicDrawContext for font draw - AZ::RHI::Ptr dynamicDraw = RPI::DynamicDrawInterface::Get()->CreateDynamicDrawContext(scene); - - Data::Instance shader = AZ::RPI::LoadShader(shaderFilepath); - AZ::RPI::ShaderOptionList shaderOptions; - shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_useColorChannels"), AZ::Name("false"))); - shaderOptions.push_back(AZ::RPI::ShaderOption(AZ::Name("o_clamp"), AZ::Name("true"))); - dynamicDraw->InitShaderWithVariant(shader, &shaderOptions); - dynamicDraw->InitVertexFormat({{"POSITION", RHI::Format::R32G32B32_FLOAT}, {"COLOR", RHI::Format::B8G8R8A8_UNORM}, {"TEXCOORD0", RHI::Format::R32G32_FLOAT}}); - dynamicDraw->EndInit(); - - // exclusive lock while writing - AZStd::lock_guard lock(m_sceneToDynamicDrawMutex); - m_sceneToDynamicDrawMap.insert(AZStd::make_pair(scene, dynamicDraw)); - - return dynamicDraw; -} - - #endif diff --git a/Gems/AtomLyIntegration/AtomFont/Code/Source/FFont.cpp b/Gems/AtomLyIntegration/AtomFont/Code/Source/FFont.cpp index 31eb089803..1976c69b45 100644 --- a/Gems/AtomLyIntegration/AtomFont/Code/Source/FFont.cpp +++ b/Gems/AtomLyIntegration/AtomFont/Code/Source/FFont.cpp @@ -60,14 +60,7 @@ static const size_t MaxVerts = 8 * 1024; // 2048 quads static const size_t MaxIndices = (MaxVerts * 6) / 4; // 6 indices per quad, 6/4 * MaxVerts static const char DrawList2DPassName[] = "2dpass"; -namespace ShaderInputs -{ - static const char TextureIndexName[] = "m_texture"; - static const char WorldToProjIndexName[] = "m_worldToProj"; - static const char SamplerIndexName[] = "m_sampler"; -} - -AZ::FFont::FFont(AtomFont* atomFont, const char* fontName) +AZ::FFont::FFont(AZ::AtomFont* atomFont, const char* fontName) : m_name(fontName) , m_atomFont(atomFont) { @@ -78,9 +71,14 @@ AZ::FFont::FFont(AtomFont* atomFont, const char* fontName) FontEffect* effect = AddEffect("default"); effect->AddPass(); - AddRef(); + // Create cpu memory to cache the font draw data before submit + m_vertexBuffer = new SVF_P3F_C4B_T2F[MaxVerts]; + m_indexBuffer = new u16[MaxIndices]; - AZ::Render::Bootstrap::NotificationBus::Handler::BusConnect(); + m_vertexCount = 0; + m_indexCount = 0; + + AddRef(); } AZ::RPI::ViewportContextPtr AZ::FFont::GetDefaultViewportContext() const @@ -98,55 +96,10 @@ AZ::RPI::WindowContextSharedPtr AZ::FFont::GetDefaultWindowContext() const return {}; } -bool AZ::FFont::InitFont(AZ::RPI::Scene* renderScene) -{ - if (!renderScene) - { - return false; - } - - auto initializationState = InitializationState::Uninitialized; - // Do an atomic transition to Initializing if we're in the Uninitialized state. - // Otherwise, check the current state. - // If we're Initialized, there's no more work to be done, return true to indicate we're good to go. - // If we're Initializing (on another thread), return false to let the consumer know it's not safe for us to be used yet. - if (!m_fontInitializationState.compare_exchange_strong(initializationState, InitializationState::Initializing)) - { - return initializationState == InitializationState::Initialized; - } - - // Create and initialize DynamicDrawContext for font draw - AZ::RPI::Ptr dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(renderScene); - - // Save draw srg input indices for later use - Data::Instance drawSrg = dynamicDraw->NewDrawSrg(); - const RHI::ShaderResourceGroupLayout* layout = drawSrg->GetAsset()->GetLayout(); - - m_fontShaderData.m_imageInputIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::TextureIndexName)); - AZ_Error("AtomFont::FFont", m_fontShaderData.m_imageInputIndex.IsValid(), "Failed to find shader input constant %s.", - ShaderInputs::TextureIndexName); - - m_fontShaderData.m_viewProjInputIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::WorldToProjIndexName)); - AZ_Error("AtomFont::FFont", m_fontShaderData.m_viewProjInputIndex.IsValid(), "Failed to find shader input constant %s.", - ShaderInputs::WorldToProjIndexName); - - // Create cpu memory to cache the font draw data before submit - m_vertexBuffer = new SVF_P3F_C4B_T2F[MaxVerts]; - m_indexBuffer = new u16[MaxIndices]; - - m_vertexCount = 0; - m_indexCount = 0; - - m_fontInitializationState = InitializationState::Initialized; - return true; -} - AZ::FFont::~FFont() { AZ_Assert(m_atomFont == nullptr, "The font should already be unregistered through a call to AZ::FFont::Release()"); - AZ::Render::Bootstrap::NotificationBus::Handler::BusDisconnect(); - delete[] m_vertexBuffer; delete[] m_indexBuffer; @@ -303,7 +256,8 @@ void AZ::FFont::DrawStringUInternal( const TextDrawContext& ctx) { // Lazily ensure we're initialized before attempting to render. - if (!viewportContext || !InitFont(viewportContext->GetRenderScene().get())) + // Validate that there is a render scene before attempting to init. + if (!viewportContext || !viewportContext->GetRenderScene()) { return; } @@ -323,12 +277,6 @@ void AZ::FFont::DrawStringUInternal( return; } - // if the font is about to be deleted then m_atomFont can be nullptr - if (!m_atomFont) - { - return; - } - const bool orthoMode = ctx.m_overrideViewProjMatrices; const float viewX = viewport.m_minX; @@ -406,14 +354,17 @@ void AZ::FFont::DrawStringUInternal( if (numQuads) { - auto dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(viewportContext->GetRenderScene().get()); - //setup per draw srg - auto drawSrg = dynamicDraw->NewDrawSrg(); - drawSrg->SetConstant(m_fontShaderData.m_viewProjInputIndex, modelViewProjMat); - drawSrg->SetImageView(m_fontShaderData.m_imageInputIndex, m_fontStreamingImage->GetImageView()); - drawSrg->Compile(); + AZ::RPI::Ptr dynamicDraw = AZ::AtomBridge::PerViewportDynamicDraw::Get()->GetDynamicDrawContextForViewport(m_dynamicDrawContextName, viewportContext->GetId()); + if (dynamicDraw) + { + //setup per draw srg + auto drawSrg = dynamicDraw->NewDrawSrg(); + drawSrg->SetConstant(m_fontShaderData.m_viewProjInputIndex, modelViewProjMat); + drawSrg->SetImageView(m_fontShaderData.m_imageInputIndex, m_fontStreamingImage->GetImageView()); + drawSrg->Compile(); - dynamicDraw->DrawIndexed(m_vertexBuffer, m_vertexCount, m_indexBuffer, m_indexCount, RHI::IndexFormat::Uint16, drawSrg); + dynamicDraw->DrawIndexed(m_vertexBuffer, m_vertexCount, m_indexBuffer, m_indexCount, RHI::IndexFormat::Uint16, drawSrg); + } m_indexCount = 0; m_vertexCount = 0; } @@ -694,12 +645,6 @@ uint32_t AZ::FFont::WriteTextQuadsToBuffers(SVF_P2F_C4B_T2F_F4B* verts, uint16_t return numQuadsWritten; } - // if the font is about to be deleted then m_atomFont can be nullptr - if (!m_atomFont) - { - return numQuadsWritten; - } - SVF_P2F_C4B_T2F_F4B* vertexData = verts; uint16_t* indexData = indices; size_t vertexOffset = 0; @@ -1523,7 +1468,7 @@ bool AZ::FFont::UpdateTexture() { using namespace AZ; - if (m_fontInitializationState != InitializationState::Initialized || !m_fontImage) + if (!m_fontImage) { return false; } @@ -1591,7 +1536,7 @@ void AZ::FFont::Prepare(const char* str, bool updateTexture, const AtomFont::Gly const bool rerenderGlyphs = m_sizeBehavior == SizeBehavior::Rerender; const AtomFont::GlyphSize usedGlyphSize = rerenderGlyphs ? glyphSize : AtomFont::defaultGlyphSize; bool texUpdateNeeded = m_fontTexture->PreCacheString(str, nullptr, m_sizeRatio, usedGlyphSize, m_fontHintParams) == 1 || m_fontTexDirty; - if (m_fontInitializationState == InitializationState::Initialized && updateTexture && texUpdateNeeded && m_fontImage) + if (updateTexture && texUpdateNeeded && m_fontImage) { UpdateTexture(); m_fontTexDirty = false; @@ -1625,12 +1570,6 @@ void AZ::FFont::ScaleCoord(const RHI::Viewport& viewport, float& x, float& y) co y *= height / WindowScaleHeight; } - -void AZ::FFont::OnBootstrapSceneReady([[maybe_unused]] AZ::RPI::Scene* bootstrapScene) -{ - InitFont(bootstrapScene); -} - static void SetCommonContextFlags(AZ::TextDrawContext& ctx, const AzFramework::TextDrawParameters& params) { if (params.m_hAlign == AzFramework::TextHorizontalAlignment::Center)