From 8232b0efc8b0cc59a36f013aa02b10d1e2e005d1 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 16:15:23 +0200 Subject: [PATCH] Add renderer shader uniform command --- docs/modernization/build-inventory.md | 17 ++-- docs/modernization/roadmap.md | 26 ++++--- src/renderer_api/recording_renderer.cpp | 26 +++++++ src/renderer_api/recording_renderer.h | 5 ++ src/renderer_api/renderer_api.cpp | 19 +++++ src/renderer_api/renderer_api.h | 7 ++ tests/CMakeLists.txt | 2 +- tests/renderer_api/renderer_api_tests.cpp | 95 +++++++++++++++++++++++ tools/pano_cli/main.cpp | 13 ++++ 9 files changed, 189 insertions(+), 21 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 21ea273..fe536a0 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -287,15 +287,16 @@ Known local toolchain state: - `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates backend-owned resource creation, command order, scissor state, depth state, blend state, texture-slot binding, sampler-state binding, texture-upload byte - counts, readback bounds, frame-capture sources, destination buffer sizes, and - render-target blit regions, records - render/scissor/depth/blend/texture-bind/sampler-bind/upload/readback/ - frame-capture/blit commands, draw mesh inputs, and records trace markers - without a window or GL context. + counts, shader-uniform writes, readback bounds, frame-capture sources, + destination buffer sizes, and render-target blit regions, records + render/scissor/depth/blend/shader-uniform/texture-bind/sampler-bind/upload/ + readback/frame-capture/blit commands, draw mesh inputs, and records trace + markers without a window or GL context. - `pano_cli record-render` exposes the recording renderer through JSON - automation, including scissor/depth/blend/texture-bind/sampler-bind/upload/ - readback/frame-capture/blit command and byte totals, backend resource - creation counts, plus draw vertex/index totals, and is covered by + automation, including scissor/depth/blend/shader-uniform/texture-bind/ + sampler-bind/upload/readback/frame-capture/blit command and byte totals, + backend resource creation counts, plus draw vertex/index totals, and is + covered by `pano_cli_record_render_smoke` plus `pano_cli_record_render_rejects_oversized_target`. - `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory` diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 423e073..f0d00ba 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -724,9 +724,10 @@ Results: validation, texture-upload byte-count validation, frame-capture byte-size and command-order validation, render-target blit validation, texture-slot binding validation, blend-state validation, scissor-state validation, - backend-neutral resource factory validation, recording - scissor/depth/blend/texture/sampler-bind/upload/readback/frame-capture/blit - command capture, draw mesh-input capture, and invalid catalog rejection. + shader-uniform write validation, backend-neutral resource factory validation, + recording scissor/depth/blend/shader-uniform/texture/sampler-bind/upload/ + readback/frame-capture/blit command capture, draw mesh-input capture, and + invalid catalog rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. @@ -822,18 +823,19 @@ Results: - `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict renderer-owned resource factory and command-order/scissor-state/depth-state/blend-state/texture-bind/ - sampler-bind/texture-upload/readback/frame-capture/blit validation; it - creates validated textures, render targets, shaders, meshes, and readback - buffers, then records commands, trace markers, scissor state, depth state, - blend state, texture/sampler binds, draw mesh inputs, uploads/readbacks, - frame captures, and render-target blits, giving automation a backend-neutral - render path that does not require a window or GL context. + sampler-bind/shader-uniform/texture-upload/readback/frame-capture/blit + validation; it creates validated textures, render targets, shaders, meshes, + and readback buffers, then records commands, trace markers, scissor state, + depth state, blend state, shader uniform writes, texture/sampler binds, draw + mesh inputs, uploads/readbacks, frame captures, and render-target blits, + giving automation a backend-neutral render path that does not require a + window or GL context. - `pano_cli record-render` exercises that headless recording renderer and emits JSON command counts, resource creation counts, target dimensions, backend name, trace/draw summary, and draw vertex/index totals, scissor/depth/ - blend-state plus texture/sampler-bind/upload/readback/frame-capture/blit - command/byte totals for agent automation, with an expected-failure smoke for - oversized render/readback targets. + blend-state plus shader-uniform/texture/sampler-bind/upload/readback/ + frame-capture/blit command/byte totals for agent automation, with an + expected-failure smoke for oversized render/readback targets. - `pano_cli simulate-document-history` exercises pure document history apply/undo/redo behavior and emits JSON layer/frame/history state for agent automation. diff --git a/src/renderer_api/recording_renderer.cpp b/src/renderer_api/recording_renderer.cpp index c8fd326..f21e844 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -208,6 +208,30 @@ pp::foundation::Status RecordingCommandContext::bind_shader(IShaderProgram& shad return pp::foundation::Status::success(); } +pp::foundation::Status RecordingCommandContext::set_shader_uniform( + const char* name, + std::span bytes) noexcept +{ + if (!in_render_pass_) { + return pp::foundation::Status::invalid_argument("render pass has not begun"); + } + if (!shader_bound_) { + return pp::foundation::Status::invalid_argument("shader must be bound before setting uniforms"); + } + + const auto status = validate_shader_uniform_write(name, bytes); + if (!status.ok()) { + return status; + } + + push_command(commands_, RecordedRenderCommand { + .kind = RecordedRenderCommandKind::set_shader_uniform, + .uniform_bytes = static_cast(bytes.size()), + .name = name, + }); + return pp::foundation::Status::success(); +} + pp::foundation::Status RecordingCommandContext::bind_texture( std::uint32_t slot, ITexture2D& texture) noexcept @@ -574,6 +598,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no return "set_depth_state"; case RecordedRenderCommandKind::bind_shader: return "bind_shader"; + case RecordedRenderCommandKind::set_shader_uniform: + return "set_shader_uniform"; case RecordedRenderCommandKind::bind_texture: return "bind_texture"; case RecordedRenderCommandKind::bind_sampler: diff --git a/src/renderer_api/recording_renderer.h b/src/renderer_api/recording_renderer.h index 0025702..f01e695 100644 --- a/src/renderer_api/recording_renderer.h +++ b/src/renderer_api/recording_renderer.h @@ -14,6 +14,7 @@ enum class RecordedRenderCommandKind : std::uint8_t { set_blend_state, set_depth_state, bind_shader, + set_shader_uniform, bind_texture, bind_sampler, bind_mesh, @@ -50,6 +51,7 @@ struct RecordedRenderCommand { std::uint64_t capture_bytes = 0; std::uint64_t blit_source_bytes = 0; std::uint64_t blit_destination_bytes = 0; + std::uint64_t uniform_bytes = 0; const char* component = ""; const char* name = ""; }; @@ -111,6 +113,9 @@ public: [[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override; [[nodiscard]] pp::foundation::Status set_depth_state(DepthState state) noexcept override; [[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override; + [[nodiscard]] pp::foundation::Status set_shader_uniform( + const char* name, + std::span bytes) noexcept override; [[nodiscard]] pp::foundation::Status bind_texture( std::uint32_t slot, ITexture2D& texture) noexcept override; diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index 238ec5a..73c456f 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -348,6 +348,25 @@ pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noex return pp::foundation::Status::success(); } +pp::foundation::Status validate_shader_uniform_write( + const char* name, + std::span bytes) noexcept +{ + if (is_empty_c_string(name)) { + return pp::foundation::Status::invalid_argument("shader uniform name must not be empty"); + } + + if (bytes.empty()) { + return pp::foundation::Status::invalid_argument("shader uniform bytes must not be empty"); + } + + if (bytes.size() > max_shader_uniform_bytes) { + return pp::foundation::Status::out_of_range("shader uniform bytes exceed the configured limit"); + } + + return pp::foundation::Status::success(); +} + pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept { const auto extent_status = validate_extent(desc.extent); diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index aad5a46..ea6a40f 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -14,6 +14,7 @@ constexpr std::uint32_t max_mesh_vertices = 16777216; constexpr std::uint32_t max_texture_slots = 32; constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL; constexpr std::size_t max_shader_source_bytes = 4ULL * 1024ULL * 1024ULL; +constexpr std::size_t max_shader_uniform_bytes = 64ULL * 1024ULL; enum class TextureFormat : std::uint8_t { rgba8, @@ -206,6 +207,9 @@ public: [[nodiscard]] virtual pp::foundation::Status set_blend_state(BlendState state) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status set_depth_state(DepthState state) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status set_shader_uniform( + const char* name, + std::span bytes) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status bind_texture( std::uint32_t slot, ITexture2D& texture) noexcept = 0; @@ -267,6 +271,9 @@ public: [[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept; [[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept; +[[nodiscard]] pp::foundation::Status validate_shader_uniform_write( + const char* name, + std::span bytes) noexcept; [[nodiscard]] pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept; [[nodiscard]] pp::foundation::Result readback_byte_size( TextureDesc desc, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 23ba72b..543fd26 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -365,7 +365,7 @@ if(TARGET pano_cli) COMMAND pano_cli record-render --width 32 --height 16) set_tests_properties(pano_cli_record_render_smoke PROPERTIES LABELS "renderer;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"createdResources\":6.*\"commands\":16.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048") + PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"createdResources\":6.*\"commands\":17.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048") add_test(NAME pano_cli_record_render_rejects_oversized_target COMMAND "${CMAKE_COMMAND}" diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index 3c787d4..0b186c9 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -52,6 +52,7 @@ using pp::renderer::TextureDesc; using pp::renderer::TextureFormat; using pp::renderer::Viewport; using pp::renderer::max_shader_source_bytes; +using pp::renderer::max_shader_uniform_bytes; using pp::renderer::max_texture_dimension; using pp::renderer::max_texture_slots; using pp::renderer::panopainter_shader_catalog; @@ -77,6 +78,7 @@ using pp::renderer::validate_sampler_filter; using pp::renderer::validate_scissor; using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_program_desc; +using pp::renderer::validate_shader_uniform_write; using pp::renderer::validate_texture_slot; using pp::renderer::validate_viewport; @@ -223,6 +225,24 @@ public: return pp::foundation::Status::success(); } + [[nodiscard]] pp::foundation::Status set_shader_uniform( + const char* name, + std::span bytes) noexcept override + { + if (!in_render_pass) { + return pp::foundation::Status::invalid_argument("render pass has not begun"); + } + + const auto status = validate_shader_uniform_write(name, bytes); + if (!status.ok()) { + return status; + } + + last_uniform_name = name; + last_uniform_bytes = bytes.size(); + return pp::foundation::Status::success(); + } + [[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override { if (!in_render_pass) { @@ -386,6 +406,8 @@ public: bool in_render_pass = false; const char* shader_name = nullptr; + const char* last_uniform_name = nullptr; + std::size_t last_uniform_bytes = 0; ScissorRect last_scissor {}; BlendState last_blend_state {}; DepthState last_depth_state {}; @@ -829,6 +851,30 @@ void validates_shader_program_descriptors(pp::tests::Harness& h) PP_EXPECT(h, excessive_source_status.code == StatusCode::out_of_range); } +void validates_shader_uniform_writes(pp::tests::Harness& h) +{ + const std::array matrix_bytes {}; + const std::array one_byte {}; + static const std::array excessive_bytes {}; + + PP_EXPECT(h, validate_shader_uniform_write("mvp", matrix_bytes).ok()); + PP_EXPECT(h, validate_shader_uniform_write("opacity", one_byte).ok()); + + const auto null_name = validate_shader_uniform_write(nullptr, matrix_bytes); + const auto empty_name = validate_shader_uniform_write("", matrix_bytes); + const auto empty_bytes = validate_shader_uniform_write("mvp", std::span {}); + const auto excessive = validate_shader_uniform_write("mvp", excessive_bytes); + + PP_EXPECT(h, !null_name.ok()); + PP_EXPECT(h, null_name.code == StatusCode::invalid_argument); + PP_EXPECT(h, !empty_name.ok()); + PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); + PP_EXPECT(h, !empty_bytes.ok()); + PP_EXPECT(h, empty_bytes.code == StatusCode::invalid_argument); + PP_EXPECT(h, !excessive.ok()); + PP_EXPECT(h, excessive.code == StatusCode::out_of_range); +} + void validates_panopainter_shader_catalog(pp::tests::Harness& h) { const auto catalog = panopainter_shader_catalog(); @@ -895,6 +941,7 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) FakeTexture texture; FakeReadbackBuffer readback_buffer(64U * 32U * 4U); const std::array upload_bytes {}; + const std::array uniform_bytes {}; FakeShaderProgram shader; FakeMesh mesh; @@ -920,6 +967,7 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) }) .ok()); PP_EXPECT(h, context.bind_shader(shader).ok()); + PP_EXPECT(h, context.set_shader_uniform("mvp", uniform_bytes).ok()); PP_EXPECT(h, context.bind_texture(2, texture).ok()); PP_EXPECT(h, context.bind_sampler(2, SamplerDesc { .min_filter = SamplerFilter::linear, @@ -962,6 +1010,8 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) PP_EXPECT(h, device.context.last_depth_state.test_enabled); PP_EXPECT(h, device.context.last_depth_state.write_enabled); PP_EXPECT(h, device.context.last_depth_state.compare == CompareOp::less_or_equal); + PP_EXPECT(h, device.context.last_uniform_name == std::string_view("mvp")); + PP_EXPECT(h, device.context.last_uniform_bytes == 64U); PP_EXPECT(h, device.context.last_texture_slot == 2U); PP_EXPECT(h, device.context.last_texture_bytes == 8192U); PP_EXPECT(h, device.context.last_sampler_slot == 2U); @@ -1043,6 +1093,49 @@ void render_devices_create_validated_resources(pp::tests::Harness& h) PP_EXPECT(h, bad_readback.status().code == StatusCode::invalid_argument); } +void recording_renderer_records_shader_uniform_writes(pp::tests::Harness& h) +{ + RecordingRenderDevice device; + RecordingRenderTarget target(TextureDesc { + .extent = Extent2D { .width = 16, .height = 8 }, + .format = TextureFormat::rgba8, + .render_target = true, + }); + RecordingShaderProgram shader("uniform-shader"); + const std::array uniform_bytes {}; + + auto& context = device.immediate_context(); + const auto before_begin = context.set_shader_uniform("mvp", uniform_bytes); + PP_EXPECT(h, !before_begin.ok()); + PP_EXPECT(h, before_begin.code == StatusCode::invalid_argument); + + PP_EXPECT(h, context.begin_render_pass(target, ClearColor {}).ok()); + const auto before_shader = context.set_shader_uniform("mvp", uniform_bytes); + PP_EXPECT(h, !before_shader.ok()); + PP_EXPECT(h, before_shader.code == StatusCode::invalid_argument); + + PP_EXPECT(h, context.bind_shader(shader).ok()); + PP_EXPECT(h, context.set_shader_uniform("mvp", uniform_bytes).ok()); + const auto empty_name = context.set_shader_uniform("", uniform_bytes); + const auto empty_bytes = context.set_shader_uniform("mvp", std::span {}); + context.end_render_pass(); + + PP_EXPECT(h, !empty_name.ok()); + PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); + PP_EXPECT(h, !empty_bytes.ok()); + PP_EXPECT(h, empty_bytes.code == StatusCode::invalid_argument); + + const auto commands = device.commands(); + PP_EXPECT(h, commands.size() == 4U); + PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::begin_render_pass); + PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::bind_shader); + PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_shader_uniform); + PP_EXPECT(h, commands[2].name == std::string_view("mvp")); + PP_EXPECT(h, commands[2].uniform_bytes == 64U); + PP_EXPECT(h, recorded_render_command_kind_name(commands[2].kind) == std::string_view("set_shader_uniform")); + PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::end_render_pass); +} + void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) { RecordingRenderDevice device; @@ -1485,10 +1578,12 @@ int main() harness.run("validates_sampler_contract", validates_sampler_contract); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); + harness.run("validates_shader_uniform_writes", validates_shader_uniform_writes); harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); harness.run("rejects_invalid_shader_catalogs", rejects_invalid_shader_catalogs); harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch); harness.run("render_devices_create_validated_resources", render_devices_create_validated_resources); + harness.run("recording_renderer_records_shader_uniform_writes", recording_renderer_records_shader_uniform_writes); harness.run("recording_renderer_records_valid_command_sequences", recording_renderer_records_valid_command_sequences); harness.run("recording_renderer_rejects_invalid_command_order_and_targets", recording_renderer_rejects_invalid_command_order_and_targets); return harness.finish(); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index ba6b3c0..a38e4fb 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2234,6 +2234,7 @@ int record_render(int argc, char** argv) std::byte { 0xff }, std::byte { 0xff }, }; + const std::array uniform_mvp {}; static constexpr char shader_source[] = "void main() {}"; const auto shader = device.create_shader_program(pp::renderer::ShaderProgramDesc { .debug_name = "pano-cli-record-render", @@ -2322,6 +2323,7 @@ int record_render(int argc, char** argv) } const auto shader_status = context.bind_shader(*shader.value()); + const auto uniform_status = context.set_shader_uniform("mvp", uniform_mvp); const auto blend_status = context.set_blend_state(pp::renderer::BlendState { .enabled = true, .source_color = pp::renderer::BlendFactor::source_alpha, @@ -2351,6 +2353,10 @@ int record_render(int argc, char** argv) print_error("record-render", shader_status.message); return 2; } + if (!uniform_status.ok()) { + print_error("record-render", uniform_status.message); + return 2; + } if (!blend_status.ok()) { print_error("record-render", blend_status.message); return 2; @@ -2421,6 +2427,7 @@ int record_render(int argc, char** argv) std::size_t scissor_commands = 0; std::size_t blend_commands = 0; std::size_t depth_commands = 0; + std::size_t uniform_commands = 0; std::size_t bind_texture_commands = 0; std::size_t bind_sampler_commands = 0; std::size_t upload_commands = 0; @@ -2430,6 +2437,7 @@ int record_render(int argc, char** argv) std::size_t trace_markers = 0; std::uint64_t draw_vertices = 0; std::uint64_t draw_indices = 0; + std::uint64_t uniform_bytes = 0; std::uint64_t upload_bytes = 0; std::uint64_t bound_texture_bytes = 0; std::uint64_t readback_bytes = 0; @@ -2448,6 +2456,9 @@ int record_render(int argc, char** argv) ++blend_commands; } else if (command.kind == pp::renderer::RecordedRenderCommandKind::set_depth_state) { ++depth_commands; + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::set_shader_uniform) { + ++uniform_commands; + uniform_bytes += command.uniform_bytes; } else if (command.kind == pp::renderer::RecordedRenderCommandKind::bind_texture) { ++bind_texture_commands; const auto bound_bytes = pp::renderer::texture_byte_size(command.texture_desc); @@ -2487,6 +2498,8 @@ int record_render(int argc, char** argv) << ",\"scissorCommands\":" << scissor_commands << ",\"blendCommands\":" << blend_commands << ",\"depthCommands\":" << depth_commands + << ",\"uniformCommands\":" << uniform_commands + << ",\"uniformBytes\":" << uniform_bytes << ",\"bindTextureCommands\":" << bind_texture_commands << ",\"bindSamplerCommands\":" << bind_sampler_commands << ",\"boundTextureBytes\":" << bound_texture_bytes