Skip to content

Commit 4dae638

Browse files
committed
Add Nodes for Opening and Closing Projects
Added some more project workflow management to RTX Remix nodes to handle execution flow. Mainly to save VRAM for AI processing that remix is taking up in viewport. New Features: • Open Project Node - Opens RTX Remix projects by layer ID • Get Loaded Project - Gets layer ID of current open project for Open Project node to use later • Close Project Node - Closes RTX Remix projects • Get Default Directory Node - Captures the RTX Remix default output directory before closing projects • Edited Ingest Texture Node - Made Optional Output_folder mandatory, provided by user or Get Default Direcotry Node • Fixed paths returned by Remix Set Edit Target to use quote instead of quote_plus for compatibilty for paths with spaces calling toolkit • Added a PBRify workflow "rtx_remix_pbrify_workflow_LowVRAM" showing this at work and for users with low vram • Edited normal PBRify workflow to work with new ingest texture changes
1 parent 22b72c9 commit 4dae638

File tree

8 files changed

+10401
-294
lines changed

8 files changed

+10401
-294
lines changed

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
### Changed
13-
- Update nodes to be compatible with the renamed models
1413

1514
### Fixed
1615

1716
### Removed
1817

18+
## [1.1.0] - 2025-07-24
19+
20+
### Added
21+
- **Open Project Node**: Opens RTX Remix projects by layer ID for workflow management
22+
- **Get Loaded Project Node**: Gets layer ID of current open project for use with Open Project node
23+
- **Close Project Node**: Closes RTX Remix projects and a force boolean parameter default false
24+
- **Get Default Directory Node**: Captures the RTX Remix default output directory before closing projects
25+
- Low VRAM workflow example: "rtx_remix_pbrify_workflow_LowVRAM.json" demonstrating new project management nodes
26+
27+
### Changed
28+
- **Ingest Texture Node**: Made output_folder parameter mandatory, must be provided by user or Get Default Directory Node
29+
- Updated normal PBRify workflow to work with new ingest texture changes
30+
31+
### Fixed
32+
- Path handling compatibility for URLs with spaces when calling toolkit
33+
34+
### Removed
35+
1936
## [2024.0.0]
2037

2138
### Added

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ You should see RTX Remix nodes:
1919
- **RTX Remix Texture Types**: Select multiple texture types from a list of supported texture types.
2020

2121
### Ingestion
22-
- **RTX Remix Ingest Texture**: Ingest an image as a texture and save it to disk
22+
- **RTX Remix Ingest Texture**: Ingest an image as a texture and save it to disk. Output folder is now mandatory and must be provided by user or Get Default Directory node
23+
- **RTX Remix Get Default Directory**: Captures the RTX Remix default output directory before closing projects for use with texture ingestion
2324

2425
### Common
2526
- **RTX Remix Rest API Details**: Provide the port information to connect to the RTX Remix Toolkit
@@ -38,6 +39,9 @@ You should see RTX Remix nodes:
3839
- **RTX Remix Get Edit Target**: Get the edit target from the currently open project
3940
- **RTX Remix Layer Types**: Select multiple layer types from a list of supported layer types.
4041
- **RTX Remix Layer Type**: Select from a list of supported layer types.
42+
- **RTX Remix Open Project**: Opens RTX Remix projects by layer ID for workflow management and VRAM optimization
43+
- **RTX Remix Get Loaded Project**: Gets layer ID of current open project for use with Open Project node
44+
- **RTX Remix Close Project**: Closes RTX Remix projects to save VRAM during AI processing
4145

4246
## Example Workflows and Templates
4347

nodes/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@
2626
Switch,
2727
)
2828
from .file import DeleteFile
29-
from .ingestion import IngestTexture
29+
from .ingestion import GetDefaultDirectory, IngestTexture
3030
from .layers import (
31+
CloseProject,
3132
CreateLayer,
3233
DefineLayerId,
3334
GetEditTarget,
3435
GetLayers,
36+
GetLoadedProject,
3537
LayerType,
3638
LayerTypes,
3739
MuteLayer,
40+
OpenProject,
3841
RemoveLayer,
3942
SaveLayer,
4043
SetEditTarget,
@@ -50,18 +53,22 @@
5053
# A dictionary that contains all nodes you want to export with their names
5154
# NOTE: names should be globally unique
5255
NODE_CLASS_MAPPINGS = {
56+
"RTXRemixCloseProject": CloseProject,
5357
"RTXRemixCreateLayer": CreateLayer,
5458
"RTXRemixDefineLayerId": DefineLayerId,
5559
"RTXRemixDeleteFile": DeleteFile,
5660
"RTXRemixEndContext": EndContext,
61+
"RTXRemixGetDefaultDirectory": GetDefaultDirectory,
5762
"RTXRemixGetEditTarget": GetEditTarget,
5863
"RTXRemixGetLayers": GetLayers,
64+
"RTXRemixGetLoadedProject": GetLoadedProject,
5965
"RTXRemixGetTextures": GetTextures,
6066
"RTXRemixIngestTexture": IngestTexture,
6167
"RTXRemixInvertBool": InvertBool,
6268
"RTXRemixLayerType": LayerType,
6369
"RTXRemixLayerTypes": LayerTypes,
6470
"RTXRemixMuteLayer": MuteLayer,
71+
"RTXRemixOpenProject": OpenProject,
6572
"RTXRemixRemoveLayer": RemoveLayer,
6673
"RTXRemixRestAPIDetails": RestAPIDetails,
6774
"RTXRemixSaveLayer": SaveLayer,
@@ -79,18 +86,22 @@
7986

8087
# A dictionary that contains the friendly/humanly readable titles for the nodes
8188
NODE_DISPLAY_NAME_MAPPINGS = {
89+
"RTXRemixCloseProject": "RTX Remix Close Project",
8290
"RTXRemixCreateLayer": "RTX Remix Create Layer",
8391
"RTXRemixDefineLayerId": "RTX Remix Define Layer ID",
8492
"RTXRemixDeleteFile": "RTX Remix Delete File",
8593
"RTXRemixEndContext": "RTX Remix End Context",
94+
"RTXRemixGetDefaultDirectory": "RTX Remix Get Default Directory",
8695
"RTXRemixGetEditTarget": "RTX Remix Get Edit Target",
8796
"RTXRemixGetLayers": "RTX Remix Get Layers",
97+
"RTXRemixGetLoadedProject": "RTX Remix Get Loaded Project",
8898
"RTXRemixGetTextures": "RTX Remix Get Textures",
8999
"RTXRemixIngestTexture": "RTX Remix Ingest Texture",
90100
"RTXRemixInvertBool": "RTX Remix Invert Boolean Value",
91101
"RTXRemixLayerType": "RTX Remix Layer Type",
92102
"RTXRemixLayerTypes": "RTX Remix Layer Types",
93103
"RTXRemixMuteLayer": "RTX Remix Mute Layer",
104+
"RTXRemixOpenProject": "RTX Remix Open Project",
94105
"RTXRemixRemoveLayer": "RTX Remix Remove Layer",
95106
"RTXRemixRestAPIDetails": "RTX Remix Rest API Details",
96107
"RTXRemixSaveLayer": "RTX Remix Save Layer",

nodes/ingestion.py

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import json
1919
import os
2020
import pathlib
21+
from urllib.parse import quote_plus
2122

2223
import folder_paths
2324
import numpy as np
@@ -27,7 +28,7 @@
2728

2829
from .common import add_context_input_enabled_and_output
2930
from .constant import HEADER_LSS_REMIX_VERSION_1_0, PREFIX_MENU
30-
from .utils import check_response_status_code
31+
from .utils import check_response_status_code, posix
3132

3233
_file_name = pathlib.Path(__file__).stem
3334

@@ -57,21 +58,12 @@ def INPUT_TYPES(cls): # noqa N802
5758
"forceInput": True,
5859
},
5960
),
60-
},
61-
"optional": {
62-
"enable_override_output_folder": (
63-
"BOOLEAN",
64-
{
65-
"default": False,
66-
"label_on": "enabled",
67-
"label_off": "disabled",
68-
},
69-
),
70-
"override_output_folder": (
61+
"output_directory": (
7162
"STRING",
7263
{
7364
# node
7465
"default": "",
66+
"forceInput": True,
7567
},
7668
),
7769
},
@@ -91,24 +83,14 @@ def ingest_texture(
9183
texture: torch.Tensor,
9284
texture_type: str,
9385
texture_name: str,
94-
enable_override_output_folder: bool,
95-
override_output_folder: str,
86+
output_directory: str,
9687
):
9788
if not self.enable_this_node: # noqa
9889
return ("",)
9990
address, port = self.context # noqa
100-
if enable_override_output_folder:
101-
if not pathlib.Path(override_output_folder).exists():
102-
raise FileNotFoundError("Can't overwrite output folder, folder doesn't exist.")
103-
output_folder = override_output_folder
104-
else:
105-
# call RestAPI to get the default output folder
106-
r = requests.get(
107-
f"http://{address}:{port}/stagecraft/assets/default-directory",
108-
headers=HEADER_LSS_REMIX_VERSION_1_0,
109-
)
110-
check_response_status_code(r)
111-
output_folder = json.loads(r.text).get("directory_path", {})
91+
if not pathlib.Path(output_directory).exists():
92+
raise FileNotFoundError(f"Output directory doesn't exist: {output_directory}")
93+
output_folder = output_directory
11294

11395
full_output_folder, filename, _counter, _subfolder, _filename_prefix = folder_paths.get_save_image_path(
11496
texture_name, folder_paths.get_output_directory(), texture[0].shape[1], texture[0].shape[0]
@@ -161,3 +143,40 @@ def ingest_texture(
161143
raise FileNotFoundError(f"Can't find the texture {result_path}")
162144

163145
return (str(result_path),)
146+
147+
148+
@add_context_input_enabled_and_output
149+
class GetDefaultDirectory:
150+
"""Get the default directory from RTX Remix before closing project"""
151+
152+
@classmethod
153+
def INPUT_TYPES(cls): # noqa N802
154+
inputs = {
155+
"required": {},
156+
}
157+
return inputs
158+
159+
RETURN_TYPES = ("STRING",)
160+
RETURN_NAMES = ("default_directory",)
161+
162+
FUNCTION = "get_default_directory"
163+
164+
OUTPUT_NODE = False
165+
166+
CATEGORY = f"{PREFIX_MENU}/{_file_name}"
167+
168+
def _get_default_output_directory(self) -> str:
169+
"""Utility method to get default output directory from RTX Remix API."""
170+
address, port = self.context # noqa
171+
r = requests.get(
172+
f"http://{address}:{port}/stagecraft/assets/default-directory",
173+
headers=HEADER_LSS_REMIX_VERSION_1_0,
174+
)
175+
check_response_status_code(r)
176+
return json.loads(r.text).get("directory_path", "")
177+
178+
def get_default_directory(self):
179+
if not self.enable_this_node: # noqa
180+
return ("",)
181+
default_directory = self._get_default_output_directory()
182+
return (default_directory,)

nodes/layers.py

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import json
2121
import pathlib
2222
import re
23-
from urllib.parse import quote_plus, unquote
23+
from urllib.parse import quote, unquote
2424

2525
import requests
2626

@@ -329,7 +329,7 @@ def execute(
329329
address, port = self.context # noqa
330330
if parent_layer_id:
331331
r = requests.get(
332-
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(parent_layer_id))}/sublayers",
332+
f"http://{address}:{port}/stagecraft/layers/{quote(posix(parent_layer_id), safe='')}/sublayers",
333333
params=params,
334334
headers=HEADER_LSS_REMIX_VERSION_1_0,
335335
)
@@ -433,7 +433,7 @@ def execute(self, layer_id: str, mute: bool) -> tuple[str]: # noqa
433433
payload = {"value": mute}
434434
address, port = self.context # noqa
435435
r = requests.put(
436-
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(layer_id))}/mute",
436+
f"http://{address}:{port}/stagecraft/layers/{quote(posix(layer_id), safe='')}/mute",
437437
data=json.dumps(payload),
438438
headers=HEADER_LSS_REMIX_VERSION_1_0,
439439
)
@@ -464,7 +464,7 @@ def execute(self, layer_id: str, parent_layer_id: str) -> tuple[str]: # noqa
464464
payload = {"parent_layer_id": posix(parent_layer_id)}
465465
address, port = self.context # noqa
466466
r = requests.delete(
467-
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(layer_id))}",
467+
f"http://{address}:{port}/stagecraft/layers/{quote(posix(layer_id), safe='')}",
468468
data=json.dumps(payload),
469469
headers=HEADER_LSS_REMIX_VERSION_1_0,
470470
)
@@ -481,7 +481,7 @@ def execute(self, layer_id: str) -> tuple[str]:
481481
return ("",)
482482
address, port = self.context # noqa
483483
r = requests.post(
484-
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(layer_id))}/save",
484+
f"http://{address}:{port}/stagecraft/layers/{quote(posix(layer_id), safe='')}/save",
485485
headers=HEADER_LSS_REMIX_VERSION_1_0,
486486
)
487487
check_response_status_code(r)
@@ -531,8 +531,98 @@ def execute(self, layer_id: str) -> tuple[str]:
531531
return ("",)
532532
address, port = self.context # noqa
533533
r = requests.put(
534-
f"http://{address}:{port}/stagecraft/layers/target/{quote_plus(posix(layer_id))}",
534+
f"http://{address}:{port}/stagecraft/layers/target/{quote(posix(layer_id), safe='')}",
535535
headers=HEADER_LSS_REMIX_VERSION_1_0,
536536
)
537537
check_response_status_code(r)
538538
return (layer_id,) # return an output so that you can make sure this executes before other nodes
539+
540+
541+
@add_context_input_enabled_and_output
542+
class CloseProject:
543+
"""Close the currently open project"""
544+
545+
@classmethod
546+
def INPUT_TYPES(cls): # noqa N802
547+
inputs = get_context_inputs()
548+
inputs["optional"] = {
549+
"force": ("BOOLEAN", {"default": False}),
550+
}
551+
return inputs
552+
553+
RETURN_TYPES = ("STRING",)
554+
RETURN_NAMES = ("status",)
555+
556+
FUNCTION = "close_project"
557+
558+
OUTPUT_NODE = False
559+
560+
CATEGORY = f"{PREFIX_MENU}/{_file_name}"
561+
562+
def close_project(self, force: bool = False) -> tuple[str]:
563+
if not self.enable_this_node: # noqa
564+
return ("disabled",)
565+
address, port = self.context # noqa
566+
url = f"http://{address}:{port}/stagecraft/project"
567+
if force:
568+
url += "?force=true"
569+
r = requests.delete(
570+
url,
571+
headers=HEADER_LSS_REMIX_VERSION_1_0,
572+
)
573+
check_response_status_code(r)
574+
return ("closed",)
575+
576+
577+
@add_context_input_enabled_and_output
578+
class OpenProject(_LayerOp):
579+
"""Open a project using the specified layer ID"""
580+
581+
def execute(self, layer_id: str) -> tuple[str]:
582+
if not self.enable_this_node: # noqa
583+
return ("",)
584+
address, port = self.context # noqa
585+
r = requests.put(
586+
f"http://{address}:{port}/stagecraft/project/{quote(posix(layer_id), safe='')}",
587+
headers=HEADER_LSS_REMIX_VERSION_1_0,
588+
)
589+
check_response_status_code(r)
590+
return (layer_id,)
591+
592+
593+
@add_context_input_enabled_and_output
594+
class GetLoadedProject:
595+
"""Get the currently loaded project"""
596+
597+
@classmethod
598+
def INPUT_TYPES(cls): # noqa N802
599+
return get_context_inputs()
600+
601+
RETURN_TYPES = ("STRING",)
602+
RETURN_NAMES = ("layer_id",)
603+
604+
FUNCTION = "get_loaded_project"
605+
606+
OUTPUT_NODE = False
607+
608+
CATEGORY = f"{PREFIX_MENU}/{_file_name}"
609+
610+
def get_loaded_project(self) -> tuple[str]:
611+
if not self.enable_this_node: # noqa
612+
return ("",)
613+
address, port = self.context # noqa
614+
r = requests.get(
615+
f"http://{address}:{port}/stagecraft/project/",
616+
headers=HEADER_LSS_REMIX_VERSION_1_0,
617+
)
618+
check_response_status_code(r)
619+
response_data = json.loads(r.text)
620+
layer_id = response_data.get("layer_id", "")
621+
return (layer_id,)
622+
623+
@classmethod
624+
def IS_CHANGED(cls, **kwargs): # noqa N802
625+
"""
626+
Always process the node in case the loaded project in the RTX Remix app changed
627+
"""
628+
return float("nan")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "comfyui-rtx-remix"
33
description = "Use ComfyUI with RTX Remix to remaster classic games [a/https://github.com/NVIDIAGameWorks/rtx-remix](https://github.com/NVIDIAGameWorks/rtx-remix)"
4-
version = "1.0.1"
4+
version = "1.1.0"
55
license = {file = "LICENSE"}
66
dependencies = ["numpy", "pillow>=10.0.1", "requests", "torch"]
77

0 commit comments

Comments
 (0)