Merge branch 'main' into cpack_installer

This commit is contained in:
scottr
2021-06-02 18:15:24 -07:00
18 changed files with 908 additions and 168 deletions
+2
View File
@@ -10,3 +10,5 @@
#
add_subdirectory(Code)
add_subdirectory(Tools)
+23
View File
@@ -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()
+39
View File
@@ -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
+10
View File
@@ -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.
"""
@@ -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)
@@ -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.
"""
@@ -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')
@@ -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)
@@ -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
}
}
]
}
}
}
@@ -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"
}
]
}
}
}
@@ -0,0 +1,6 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassData": {
}
}
@@ -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)
+33
View File
@@ -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'])
)
@@ -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
)
################################################################################
@@ -27,11 +27,14 @@
#include <AzFramework/Scene/SceneSystemInterface.h>
#include <Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h>
#include <AtomBridge/PerViewportDynamicDrawInterface.h>
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<AZ::RPI::DynamicDrawContext> GetOrCreateDynamicDrawForScene(AZ::RPI::Scene* scene);
public:
void UnregisterFont(const char* fontName);
@@ -108,8 +104,6 @@ namespace AZ
using FontFamilyMap = AZStd::unordered_map<AZStd::string, AZStd::weak_ptr<FontFamily>>;
using FontFamilyReverseLookupMap = AZStd::unordered_map<FontFamily*, FontFamilyMap::iterator>;
using SceneToDynamicDrawMap = AZStd::unordered_map<AZ::RPI::Scene*, AZ::RPI::Ptr<AZ::RPI::DynamicDrawContext>>;
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<FontFamilyPtr> m_persistedFontFamilies; //!< Stores persisted fonts (if "persist font families" is enabled)
SceneToDynamicDrawMap m_sceneToDynamicDrawMap;
AZStd::shared_mutex m_sceneToDynamicDrawMutex;
};
}
#endif
@@ -42,11 +42,9 @@
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/DynamicDraw/DynamicDrawInterface.h>
#include <Atom/RPI.Public/ViewportContextBus.h>
#include <Atom/RPI.Public/WindowContext.h>
#include <Atom/RPI.Public/Image/StreamingImage.h>
#include <Atom/Bootstrap/DefaultWindowBus.h>
#include <Atom/Bootstrap/BootstrapNotificationBus.h>
struct ISystem;
namespace AZ
@@ -68,7 +66,6 @@ namespace AZ
: public IFFont
, public AZStd::intrusive_refcount<AZStd::atomic_uint, FontDeleter>
, public AzFramework::FontDrawInterface
, private AZ::Render::Bootstrap::NotificationBus::Handler
{
using ref_count = AZStd::intrusive_refcount<AZStd::atomic_uint, FontDeleter>;
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<InitializationState> 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;
@@ -354,17 +354,26 @@ AZ::AtomFont::AtomFont(ISystem* system)
#endif
AZ::Interface<AzFramework::FontQueryInterface>::Register(this);
m_sceneEventHandler = AzFramework::ISceneSystem::SceneEvent::Handler(
[this](AzFramework::ISceneSystem::EventType eventType, const AZStd::shared_ptr<AzFramework::Scene>& 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<RPI::DynamicDrawContext> drawContext)
{
if (eventType == AzFramework::ISceneSystem::EventType::ScenePendingRemoval)
{
SceneAboutToBeRemoved(*scene);
}
Data::Instance<RPI::Shader> 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<AZ::RPI::ScenePtr>();
if (rpiScene)
{
AZStd::lock_guard<AZStd::shared_mutex> lock(m_sceneToDynamicDrawMutex);
if (auto it = m_sceneToDynamicDrawMap.find(rpiScene->get()); it != m_sceneToDynamicDrawMap.end())
{
m_sceneToDynamicDrawMap.erase(it);
}
}
}
AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> AZ::AtomFont::GetOrCreateDynamicDrawForScene(AZ::RPI::Scene* scene)
{
static const char* shaderFilepath = "Shaders/SimpleTextured.azshader";
{
// shared lock while reading
AZStd::shared_lock<AZStd::shared_mutex> 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<AZ::RPI::DynamicDrawContext> dynamicDraw = RPI::DynamicDrawInterface::Get()->CreateDynamicDrawContext(scene);
Data::Instance<RPI::Shader> 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<AZStd::shared_mutex> lock(m_sceneToDynamicDrawMutex);
m_sceneToDynamicDrawMap.insert(AZStd::make_pair(scene, dynamicDraw));
return dynamicDraw;
}
#endif
@@ -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<AZ::RPI::DynamicDrawContext> dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(renderScene);
// Save draw srg input indices for later use
Data::Instance<RPI::ShaderResourceGroup> 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<AZ::RPI::DynamicDrawContext> 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)