diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index eb26031..e8ef8f0 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -285,12 +285,13 @@ Known local toolchain state: source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed legacy OpenGL implementation files. - `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates - command order, readback bounds, frame-capture sources, and destination buffer - sizes, records render/readback/frame-capture commands, and records trace - markers without a window or GL context. + command order, texture-upload byte counts, readback bounds, frame-capture + sources, and destination buffer sizes, records render/upload/readback/ + frame-capture commands, and records trace markers without a window or GL + context. - `pano_cli record-render` exposes the recording renderer through JSON - automation, including readback and frame-capture command/byte totals, and is - covered by `pano_cli_record_render_smoke` plus + automation, including upload/readback/frame-capture command and byte 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` apply/undo/redo state through JSON automation and is covered by diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 7eb1866..6a6cd2b 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -416,10 +416,10 @@ adding new backends. Status: started. `pp_renderer_api` exists as a headless renderer-neutral target with texture descriptor, byte-size, viewport, mesh, readback bounds, command context, render device, shader program descriptor, mesh, render target, -readback byte-size helpers, readback command validation, frame-capture -byte-size helpers, frame-capture command validation, trace interface -validation, and the canonical PanoPainter shader catalog now consumed by the -legacy OpenGL app initialization path. +readback byte-size helpers, texture-upload/readback command validation, +frame-capture byte-size helpers, frame-capture command validation, trace +interface validation, and the canonical PanoPainter shader catalog now consumed +by the legacy OpenGL app initialization path. `pp_renderer_gl` now exists as the first OpenGL backend library and owns pure OpenGL capability detection for framebuffer fetch, map-buffer alignment, and float texture support. It also owns the OpenGL texture upload-type mapping used @@ -719,8 +719,9 @@ Results: plus malformed payload rejection at the export boundary. - `pp_renderer_api_tests` passed, including shader descriptor validation, PanoPainter shader catalog validation, readback byte-size and command-order - validation, frame-capture byte-size and command-order validation, recording - readback/frame-capture command capture, and invalid catalog rejection. + validation, texture-upload byte-count validation, frame-capture byte-size and + command-order validation, recording upload/readback/frame-capture command + capture, and invalid catalog rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. @@ -814,13 +815,15 @@ Results: reintroduces raw `GL_*`/`WGL_*` constants outside the allowed legacy OpenGL implementation files. - `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict - command-order/readback/frame-capture validation; it records commands, trace - markers, texture readbacks, and frame captures, giving automation a - backend-neutral render path that does not require a window or GL context. + command-order/texture-upload/readback/frame-capture validation; it records + commands, trace markers, texture uploads/readbacks, and frame captures, 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, target dimensions, backend name, trace/draw summary, and - readback/frame-capture command/byte totals for agent automation, with an - expected-failure smoke for oversized render/readback targets. + texture-upload/readback/frame-capture 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 039a9e8..1d8cb2d 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -202,6 +202,34 @@ pp::foundation::Status RecordingCommandContext::read_texture( return pp::foundation::Status::success(); } +pp::foundation::Status RecordingCommandContext::upload_texture( + ITexture2D& texture, + ReadbackRegion region, + std::span rgba_or_channel_bytes) noexcept +{ + if (in_render_pass_) { + return pp::foundation::Status::invalid_argument("texture upload must be outside a render pass"); + } + + const auto desc = texture.desc(); + const auto bytes = readback_byte_size(desc, region); + if (!bytes) { + return bytes.status(); + } + + if (rgba_or_channel_bytes.size() != bytes.value()) { + return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region"); + } + + push_command(commands_, RecordedRenderCommand { + .kind = RecordedRenderCommandKind::upload_texture, + .texture_desc = desc, + .readback_region = region, + .upload_bytes = bytes.value(), + }); + return pp::foundation::Status::success(); +} + pp::foundation::Status RecordingCommandContext::capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept @@ -305,6 +333,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no return "bind_mesh"; case RecordedRenderCommandKind::draw: return "draw"; + case RecordedRenderCommandKind::upload_texture: + return "upload_texture"; case RecordedRenderCommandKind::read_texture: return "read_texture"; case RecordedRenderCommandKind::capture_frame: diff --git a/src/renderer_api/recording_renderer.h b/src/renderer_api/recording_renderer.h index e6eebbf..ed80afb 100644 --- a/src/renderer_api/recording_renderer.h +++ b/src/renderer_api/recording_renderer.h @@ -13,6 +13,7 @@ enum class RecordedRenderCommandKind : std::uint8_t { bind_shader, bind_mesh, draw, + upload_texture, read_texture, capture_frame, end_render_pass, @@ -27,6 +28,7 @@ struct RecordedRenderCommand { MeshDesc mesh_desc {}; TextureDesc texture_desc {}; ReadbackRegion readback_region {}; + std::uint64_t upload_bytes = 0; std::uint64_t readback_bytes = 0; std::uint64_t capture_bytes = 0; const char* component = ""; @@ -93,6 +95,10 @@ public: ITexture2D& texture, ReadbackRegion region, IReadbackBuffer& destination) noexcept override; + [[nodiscard]] pp::foundation::Status upload_texture( + ITexture2D& texture, + ReadbackRegion region, + std::span rgba_or_channel_bytes) noexcept override; [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept override; diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index 163e642..caa8d34 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -4,6 +4,7 @@ #include #include +#include namespace pp::renderer { @@ -126,6 +127,10 @@ public: ITexture2D& texture, ReadbackRegion region, IReadbackBuffer& destination) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status upload_texture( + ITexture2D& texture, + ReadbackRegion region, + std::span rgba_or_channel_bytes) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept = 0; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 72394a2..7cb3454 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.*\"commands\":9.*\"drawCommands\":1.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048") + PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"commands\":10.*\"drawCommands\":1.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":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 05308b3..2dd6f22 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -4,6 +4,7 @@ #include "test_harness.h" #include +#include #include using pp::foundation::StatusCode; @@ -168,6 +169,22 @@ public: return pp::foundation::Status::success(); } + [[nodiscard]] pp::foundation::Status upload_texture( + pp::renderer::ITexture2D& texture, + ReadbackRegion region, + std::span rgba_or_channel_bytes) noexcept override + { + const auto bytes = readback_byte_size(texture.desc(), region); + if (!bytes) { + return bytes.status(); + } + if (rgba_or_channel_bytes.size() != bytes.value()) { + return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region"); + } + last_upload_bytes = bytes.value(); + return pp::foundation::Status::success(); + } + [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, pp::renderer::IReadbackBuffer& destination) noexcept override @@ -190,6 +207,7 @@ public: bool in_render_pass = false; const char* shader_name = nullptr; + std::uint64_t last_upload_bytes = 0; std::uint64_t last_readback_bytes = 0; std::uint64_t last_capture_bytes = 0; }; @@ -463,6 +481,7 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) FakeRenderTarget target; FakeTexture texture; FakeReadbackBuffer readback_buffer(64U * 32U * 4U); + const std::array upload_bytes {}; FakeShaderProgram shader; FakeMesh mesh; @@ -482,6 +501,11 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) const auto draw_after_end = context.draw(); PP_EXPECT(h, !draw_after_end.ok()); PP_EXPECT(h, draw_after_end.code == StatusCode::invalid_argument); + PP_EXPECT(h, context.upload_texture( + texture, + ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, + upload_bytes) + .ok()); PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, @@ -489,6 +513,7 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) .ok()); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader")); + PP_EXPECT(h, device.context.last_upload_bytes == 80U); PP_EXPECT(h, device.context.last_readback_bytes == 80U); PP_EXPECT(h, device.context.last_capture_bytes == 8192U); } @@ -502,6 +527,7 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) .render_target = true, }); RecordingReadbackBuffer readback_buffer(64U * 32U * 4U); + const std::array upload_bytes {}; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, @@ -539,28 +565,41 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) PP_EXPECT(h, commands[6].kind == RecordedRenderCommandKind::end_render_pass); PP_EXPECT(h, recorded_render_command_kind_name(commands[5].kind) == std::string_view("draw")); + PP_EXPECT(h, context.upload_texture( + texture, + ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, + upload_bytes) + .ok()); + const auto commands_after_upload = device.commands(); + PP_EXPECT(h, commands_after_upload.size() == 8U); + PP_EXPECT(h, commands_after_upload[7].kind == RecordedRenderCommandKind::upload_texture); + PP_EXPECT(h, commands_after_upload[7].texture_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_upload[7].readback_region.x == 4U); + PP_EXPECT(h, commands_after_upload[7].upload_bytes == 96U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[7].kind) == std::string_view("upload_texture")); + PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, readback_buffer) .ok()); const auto commands_after_readback = device.commands(); - PP_EXPECT(h, commands_after_readback.size() == 8U); - PP_EXPECT(h, commands_after_readback[7].kind == RecordedRenderCommandKind::read_texture); - PP_EXPECT(h, commands_after_readback[7].texture_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_readback[7].readback_region.x == 4U); - PP_EXPECT(h, commands_after_readback[7].readback_region.height == 3U); - PP_EXPECT(h, commands_after_readback[7].readback_bytes == 96U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[7].kind) == std::string_view("read_texture")); + PP_EXPECT(h, commands_after_readback.size() == 9U); + PP_EXPECT(h, commands_after_readback[8].kind == RecordedRenderCommandKind::read_texture); + PP_EXPECT(h, commands_after_readback[8].texture_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_readback[8].readback_region.x == 4U); + PP_EXPECT(h, commands_after_readback[8].readback_region.height == 3U); + PP_EXPECT(h, commands_after_readback[8].readback_bytes == 96U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[8].kind) == std::string_view("read_texture")); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); const auto commands_after_capture = device.commands(); - PP_EXPECT(h, commands_after_capture.size() == 9U); - PP_EXPECT(h, commands_after_capture[8].kind == RecordedRenderCommandKind::capture_frame); - PP_EXPECT(h, commands_after_capture[8].target_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_capture[8].target_desc.extent.height == 32U); - PP_EXPECT(h, commands_after_capture[8].capture_bytes == 8192U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[8].kind) == std::string_view("capture_frame")); + PP_EXPECT(h, commands_after_capture.size() == 10U); + PP_EXPECT(h, commands_after_capture[9].kind == RecordedRenderCommandKind::capture_frame); + PP_EXPECT(h, commands_after_capture[9].target_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_capture[9].target_desc.extent.height == 32U); + PP_EXPECT(h, commands_after_capture[9].capture_bytes == 8192U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[9].kind) == std::string_view("capture_frame")); device.clear(); PP_EXPECT(h, device.commands().empty()); @@ -586,6 +625,8 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har }); RecordingReadbackBuffer small_readback_buffer(3U); RecordingReadbackBuffer full_readback_buffer(32U * 16U * 4U); + const std::array one_pixel_upload {}; + const std::array undersized_upload {}; RecordingShaderProgram shader("strict-shader"); RecordingMesh mesh(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles }); RecordingMesh empty_mesh(MeshDesc {}); @@ -601,6 +642,13 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, device.commands().empty()); PP_EXPECT(h, context.begin_render_pass(target, ClearColor {}).ok()); + const auto upload_during_render_pass = context.upload_texture( + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + one_pixel_upload); + PP_EXPECT(h, !upload_during_render_pass.ok()); + PP_EXPECT(h, upload_during_render_pass.code == StatusCode::invalid_argument); + const auto read_during_render_pass = context.read_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, @@ -644,6 +692,20 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !read_outside_bounds.ok()); PP_EXPECT(h, read_outside_bounds.code == StatusCode::out_of_range); + const auto upload_outside_bounds = context.upload_texture( + texture, + ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, + one_pixel_upload); + PP_EXPECT(h, !upload_outside_bounds.ok()); + PP_EXPECT(h, upload_outside_bounds.code == StatusCode::out_of_range); + + const auto upload_with_wrong_size = context.upload_texture( + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + undersized_upload); + PP_EXPECT(h, !upload_with_wrong_size.ok()); + PP_EXPECT(h, upload_with_wrong_size.code == StatusCode::invalid_argument); + const auto read_into_small_buffer = context.read_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 1846322..0c64f21 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2223,6 +2223,12 @@ int record_render(int argc, char** argv) }); pp::renderer::RecordingReadbackBuffer readback_buffer( static_cast(args.width) * args.height * 4U); + const std::array upload_pixel { + std::byte { 0xff }, + std::byte { 0x00 }, + std::byte { 0xff }, + std::byte { 0xff }, + }; pp::renderer::RecordingShaderProgram shader("pano-cli-record-render"); pp::renderer::RecordingMesh mesh(pp::renderer::MeshDesc { .vertex_count = 3, @@ -2232,6 +2238,20 @@ int record_render(int argc, char** argv) device.trace()->marker("renderer", "pano_cli_record_render"); auto& context = device.immediate_context(); + const auto upload_status = context.upload_texture( + texture, + pp::renderer::ReadbackRegion { + .x = 0, + .y = 0, + .width = 1, + .height = 1, + }, + upload_pixel); + if (!upload_status.ok()) { + print_error("record-render", upload_status.message); + return 2; + } + const auto begin_status = context.begin_render_pass( target, pp::renderer::ClearColor { .r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F }); @@ -2286,15 +2306,20 @@ int record_render(int argc, char** argv) } std::size_t draw_commands = 0; + std::size_t upload_commands = 0; std::size_t readback_commands = 0; std::size_t capture_commands = 0; std::size_t trace_markers = 0; + std::uint64_t upload_bytes = 0; std::uint64_t readback_bytes = 0; std::uint64_t capture_bytes = 0; const auto commands = device.commands(); for (const auto& command : commands) { if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) { ++draw_commands; + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::upload_texture) { + ++upload_commands; + upload_bytes += command.upload_bytes; } else if (command.kind == pp::renderer::RecordedRenderCommandKind::read_texture) { ++readback_commands; readback_bytes += command.readback_bytes; @@ -2313,6 +2338,8 @@ int record_render(int argc, char** argv) << ",\"format\":\"rgba8\"}" << ",\"commands\":" << commands.size() << ",\"drawCommands\":" << draw_commands + << ",\"uploadCommands\":" << upload_commands + << ",\"uploadBytes\":" << upload_bytes << ",\"readbackCommands\":" << readback_commands << ",\"readbackBytes\":" << readback_bytes << ",\"captureCommands\":" << capture_commands