Repository: aws-deadline/deadline-cloud-for-maya
Pull request #277: feat: Update Maya adaptor to support Redshift renderer via redshift4m…
Author: login: benl-2023
Created: 2025-05-15T22:36:02Z | Merged: 2025-05-26T17:05:40Z

Issue: https://github.com/aws-deadline/deadline-cloud-for-maya/issues/257

### What was the problem/requirement? (What/Why)
* There was no Maya Adaptor support for Redshift rendering.
* Submitting Maya renders with Redshift as the selected renderer resulted in failed jobs.

### What was the solution? (How)
* Implement the Redshift handler for Maya.
  * It builds off the `rsRender` command provided by the redshift4maya plugin.

### What is the impact of this change?
* Deadline Cloud Workers with this version of the adaptor installed will be able to run Redshift renders for Maya

### How was this change tested?
* Added new unit tests

#### Unit test result
```
Required test coverage of 41.0% reached. Total coverage: 44.54%
================================================================================================================================================== slowest 5 durations ==================================================================================================================================================
0.11s call     test/unit/deadline_adaptor_for_maya/MayaAdaptor/test_adaptor.py::TestMayaAdaptor_on_start::test_maya_init_timeout
0.03s call     test/unit/deadline_adaptor_for_maya/MayaAdaptor/test_adaptor.py::TestMayaAdaptor_on_start::test_server_init_fail
0.03s call     test/unit/deadline_adaptor_for_maya/MayaAdaptor/test_adaptor.py::TestMayaAdaptor_on_run::test_on_run
0.03s call     test/unit/deadline_adaptor_for_maya/MayaAdaptor/test_adaptor.py::TestMayaAdaptor_on_run::test_run_data_wrong_schema
0.03s call     test/unit/deadline_adaptor_for_maya/MayaAdaptor/test_adaptor.py::TestMayaAdaptor_on_start::test_maya_init_fail
================================================================================================================================================== 102 passed in 2.74s ==================================================================================================================================================
```


#### Integ test result
* Added skip marker on submitter test due to prior issue with timeout fields being populated in step environment enter/exits.

```
================================================================================================================================================== slowest 5 durations ==================================================================================================================================================
29.98s call     test/integ/test_maya_adaptors.py::TestAdaptors::test_minimal_scene_adaptor
0.00s setup    test/integ/test_maya_adaptors.py::TestAdaptors::test_minimal_scene_adaptor
0.00s teardown test/integ/test_maya_adaptors.py::TestAdaptors::test_minimal_scene_adaptor
0.00s teardown test/integ/test_maya_submitters.py::TestSubmitters::test_minimal_scene_submitter
0.00s setup    test/integ/test_maya_submitters.py::TestSubmitters::test_minimal_scene_submitter
============================================================================================================================================= 1 passed, 1 skipped in 32.63s =============================================================================================================================================
```

#### Installer test result
* Not modified

#### Did you run the "Job Bundle Output Tests"? If not, why not? If so, paste the test results here.
Yes

```
Timestamp: 2025-05-15T13:24:34.639493-07:00
Running job bundle output test: <local-workspace>/job_bundle_output_tests/layers

layers
Test succeeded

Timestamp: 2025-05-15T13:24:37.241711-07:00
Running job bundle output test:  <local-workspace>/job_bundle_output_tests/referenced_layers

referenced_layers
Test succeeded

Timestamp: 2025-05-15T13:24:37.716679-07:00
Running job bundle output test:  <local-workspace>/job_bundle_output_tests/renderman
Skipping test renderman because Renderman for Maya is not installed.
Timestamp: 2025-05-15T13:24:37.716783-07:00
Running job bundle output test:  <local-workspace>/job_bundle_output_tests/cube

cube
Test succeeded

Timestamp: 2025-05-15T13:24:38.365990-07:00
Running job bundle output test:  <local-workspace>/job_bundle_output_tests/layers_no_variation

layers_no_variation
Test succeeded

All tests passed, ran 4 total.
Timestamp: 2025-05-15T13:24:40.937010-07:00
```


### Was this change documented?

- Did you update all relevant docstrings for Python functions that you modified?
  - Yes 
- Should the README.md, DEVELOPMENT.md, or other documents in the repository's docs/ directory be updated along with your change?
  - N/A
- Should the schema files for the adaptor's init-data or run-data be updated?
  - N/A 

### Is this a breaking change?
No

----

*By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.*



Diff:
diff --git a/src/deadline/maya_adaptor/MayaAdaptor/schemas/init_data.schema.json b/src/deadline/maya_adaptor/MayaAdaptor/schemas/init_data.schema.json
index 66ac9422..647dd6e2 100644
--- a/src/deadline/maya_adaptor/MayaAdaptor/schemas/init_data.schema.json
+++ b/src/deadline/maya_adaptor/MayaAdaptor/schemas/init_data.schema.json
@@ -20,7 +20,8 @@
                 "mayaSoftware",
                 "mayaVector",
                 "vray",
-                "renderman"
+                "renderman",
+                "redshift"
             ]
         },
         "scene_file": { "type": "string" },
diff --git a/src/deadline/maya_adaptor/MayaClient/render_handlers/__init__.py b/src/deadline/maya_adaptor/MayaClient/render_handlers/__init__.py
index b0ea73d7..92d154fb 100644
--- a/src/deadline/maya_adaptor/MayaClient/render_handlers/__init__.py
+++ b/src/deadline/maya_adaptor/MayaClient/render_handlers/__init__.py
@@ -2,6 +2,7 @@
 
 from .default_maya_handler import DefaultMayaHandler
 from .arnold_handler import ArnoldHandler
+from .redshift_handler import RedshiftHandler
 from .vray_handler import VRayHandler
 from .renderman_handler import RenderManHandler
 
@@ -24,5 +25,7 @@ def get_render_handler(renderer: str = "mayaSoftware") -> DefaultMayaHandler:
         return VRayHandler()
     elif renderer == "renderman":
         return RenderManHandler()
+    elif renderer == "redshift":
+        return RedshiftHandler()
     else:
         return DefaultMayaHandler()
diff --git a/src/deadline/maya_adaptor/MayaClient/render_handlers/redshift_handler.py b/src/deadline/maya_adaptor/MayaClient/render_handlers/redshift_handler.py
new file mode 100644
index 00000000..975b653b
--- /dev/null
+++ b/src/deadline/maya_adaptor/MayaClient/render_handlers/redshift_handler.py
@@ -0,0 +1,75 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+import maya.cmds
+
+from .default_maya_handler import DefaultMayaHandler
+
+
+class RedshiftHandler(DefaultMayaHandler):
+    """
+    Render Handler for Redshift
+    """
+
+    def __init__(self):
+        """
+        Constructor for redshift handler.
+
+        * "render" and "batch" kwargs are required booleans to run the redshift renderer.
+        * "animation" kwarg is required for renderer to use defaultRenderGlobals' start and end frame settings.
+        """
+        super().__init__()
+        self.render_kwargs["animation"] = True
+        self.render_kwargs["render"] = True
+        self.render_kwargs["batch"] = True
+
+    def start_render(self, data: dict) -> None:
+        """
+        Starts a Redshift render.
+
+        Args:
+            data (dict): The data given from the Adaptor. Keys expected: ['frame', 'camera']
+
+        Raises:
+            RuntimeError: If no camera was specified and no renderable camera was found, or no frame was specified.
+        """
+        self.render_kwargs["camera"] = self.get_camera_to_render(data)
+
+        frame = data.get("frame")
+        if frame is None:
+            raise RuntimeError("MayaClient: Render without a frame number is invalid.")
+
+        maya.cmds.setAttr("defaultRenderGlobals.startFrame", frame)
+        maya.cmds.setAttr("defaultRenderGlobals.endFrame", frame)
+        maya.cmds.setAttr("defaultRenderGlobals.animation", 1)
+
+        # In order of preference, use the task's output_file_prefix, the step's output_file_prefix, or the scene file setting.
+        output_file_prefix = data.get("output_file_prefix", self.output_file_prefix)
+        if output_file_prefix:
+            maya.cmds.setAttr(
+                "defaultRenderGlobals.imageFilePrefix", output_file_prefix, type="string"
+            )
+            print(f"Set imageFilePrefix to {output_file_prefix}", flush=True)
+
+        if self.image_width is not None:
+            maya.cmds.setAttr("defaultResolution.width", self.image_width)
+            print(f"Set image width to {self.image_width}", flush=True)
+        if self.image_height is not None:
+            maya.cmds.setAttr("defaultResolution.height", self.image_height)
+            print(f"Set image height to {self.image_height}", flush=True)
+
+        maya.cmds.rsRender(**self.render_kwargs)
+        print(f"MayaClient: Finished Rendering Frame {frame}\n", flush=True)
+
+    def set_render_layer(self, data: dict) -> None:
+        """
+        Sets the render layer.
+
+        Args:
+            data (dict): The data given from the Adaptor. Keys expected: ['render_layer']
+
+        Raises:
+            RuntimeError: If the render layer cannot be found
+        """
+        render_layer_name = self.get_render_layer_to_render(data)
+        if render_layer_name:
+            maya.cmds.editRenderLayerGlobals(currentRenderLayer=render_layer_name)
diff --git a/src/deadline/maya_submitter/scene.py b/src/deadline/maya_submitter/scene.py
index 14c20702..d4644e37 100644
--- a/src/deadline/maya_submitter/scene.py
+++ b/src/deadline/maya_submitter/scene.py
@@ -23,6 +23,7 @@ class RendererNames(Enum):
     arnold = "arnold"
     vray = "vray"
     renderman = "renderman"
+    redshift = "redshift"
 
 
 class Animation:
diff --git a/test/unit/deadline_adaptor_for_maya/MayaClient/render_handlers/test_redshift_handler.py b/test/unit/deadline_adaptor_for_maya/MayaClient/render_handlers/test_redshift_handler.py
new file mode 100644
index 00000000..a381f4ac
--- /dev/null
+++ b/test/unit/deadline_adaptor_for_maya/MayaClient/render_handlers/test_redshift_handler.py
@@ -0,0 +1,46 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+from deadline.maya_adaptor.MayaClient.render_handlers.redshift_handler import RedshiftHandler
+
+
+class TestRedshiftHandler:
+    def test_init(self) -> None:
+        """
+        Validates that we add the 'error_on_arnold_license_fail' function to the action dict.
+        """
+        # WHEN
+        handler = RedshiftHandler()
+
+        # THEN
+        assert handler.render_kwargs["batch"]
+        assert handler.render_kwargs["animation"]
+        assert handler.render_kwargs["batch"]
+
+    @pytest.mark.parametrize("args", [{"image_height": 1500}])
+    def test_set_image_height(self, args: dict[str, Any]) -> None:
+        """Tests that setting the image height sets the right render kwarg"""
+        # GIVEN
+        handler = RedshiftHandler()
+
+        # WHEN
+        handler.set_image_height(args)
+
+        # THEN
+        assert handler.image_height == args["image_height"]
+
+    @pytest.mark.parametrize("args", [{"image_width": 1500}])
+    def test_set_image_width(self, args: dict[str, Any]) -> None:
+        """Tests that setting the image width sets the right render kwarg"""
+        # GIVEN
+        handler = RedshiftHandler()
+
+        # WHEN
+        handler.set_image_width(args)
+
+        # THEN
+        assert handler.image_width == args["image_width"]


Reviews:
user: login: leongdl
body: Awesome! 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T01:51:39Z
user: login: jairaws
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:58:57Z
user: login: benl-2023
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:50:36Z
user: login: jairaws
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:55:32Z
user: login: leongdl
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T17:01:28Z

Review comments:
diff_hunk: @@ -0,0 +1,69 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+import maya.cmds
+
+from .default_maya_handler import DefaultMayaHandler
+
+
+class RedshiftHandler(DefaultMayaHandler):
+    """
+    Render Handler for Redshift
+    """
+
+    def __init__(self):
+        super().__init__()
+        self.render_kwargs["animation"] = True
path: src/deadline/maya_adaptor/MayaClient/render_handlers/redshift_handler.py
user: login: jairaws
body: nit: It would be better to add a comment explaining what these are for.
created_at: 2025-05-20T15:55:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+import maya.cmds
+
+from .default_maya_handler import DefaultMayaHandler
+
+
+class RedshiftHandler(DefaultMayaHandler):
+    """
+    Render Handler for Redshift
+    """
+
+    def __init__(self):
+        super().__init__()
+        self.render_kwargs["animation"] = True
path: src/deadline/maya_adaptor/MayaClient/render_handlers/redshift_handler.py
user: login: benl-2023
body: Added comments in current revision.
created_at: 2025-05-26T16:50:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: leongdl
created_at: 2025-05-16T01:52:06Z
body: Can we update the description link this is part of: https://github.com/aws-deadline/deadline-cloud-for-maya/issues/257
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-26T16:50:53Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=aws-deadline_deadline-cloud-for-maya&pullRequest=277) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [10.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=aws-deadline_deadline-cloud-for-maya&pullRequest=277&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aws-deadline_deadline-cloud-for-maya&pullRequest=277)


author_association: NONE
user: login: leongdl
created_at: 2025-05-26T17:01:58Z
body: Sonar cube failed, due to duplicate code. I think we can dismiss it.
author_association: CONTRIBUTOR