Add renderer blend state contract

This commit is contained in:
2026-06-02 15:40:43 +02:00
parent 5dbeb0504d
commit 5226746c1a
9 changed files with 326 additions and 60 deletions

View File

@@ -285,13 +285,14 @@ Known local toolchain state:
source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed
legacy OpenGL implementation files. legacy OpenGL implementation files.
- `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates - `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates
command order, texture-slot binding, texture-upload byte counts, readback command order, blend state, texture-slot binding, texture-upload byte counts,
bounds, frame-capture sources, destination buffer sizes, and render-target readback bounds, frame-capture sources, destination buffer sizes, and
blit regions, records render/texture-bind/upload/readback/frame-capture/blit render-target blit regions, records render/blend/texture-bind/upload/
commands, and records trace markers without a window or GL context. readback/frame-capture/blit commands, and records trace markers without a
window or GL context.
- `pano_cli record-render` exposes the recording renderer through JSON - `pano_cli record-render` exposes the recording renderer through JSON
automation, including texture-bind/upload/readback/frame-capture/blit command automation, including blend/texture-bind/upload/readback/frame-capture/blit
and byte totals, and is covered by `pano_cli_record_render_smoke` plus command and byte totals, and is covered by `pano_cli_record_render_smoke` plus
`pano_cli_record_render_rejects_oversized_target`. `pano_cli_record_render_rejects_oversized_target`.
- `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory` - `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory`
apply/undo/redo state through JSON automation and is covered by apply/undo/redo state through JSON automation and is covered by

View File

@@ -418,9 +418,9 @@ with texture descriptor, byte-size, viewport, mesh, readback bounds, command
context, render device, shader program descriptor, mesh, render target, context, render device, shader program descriptor, mesh, render target,
readback byte-size helpers, texture-upload/readback command validation, readback byte-size helpers, texture-upload/readback command validation,
frame-capture byte-size helpers, frame-capture command validation, frame-capture byte-size helpers, frame-capture command validation,
render-target blit validation, texture-slot binding validation, trace render-target blit validation, texture-slot binding validation, blend-state
interface validation, and the canonical PanoPainter shader catalog now consumed validation, trace interface validation, and the canonical PanoPainter shader
by the legacy OpenGL app initialization path. catalog now consumed by the legacy OpenGL app initialization path.
`pp_renderer_gl` now exists as the first OpenGL backend library and owns pure `pp_renderer_gl` now exists as the first OpenGL backend library and owns pure
OpenGL capability detection for framebuffer fetch, map-buffer alignment, and OpenGL capability detection for framebuffer fetch, map-buffer alignment, and
float texture support. It also owns the OpenGL texture upload-type mapping used float texture support. It also owns the OpenGL texture upload-type mapping used
@@ -722,8 +722,8 @@ Results:
PanoPainter shader catalog validation, readback byte-size and command-order PanoPainter shader catalog validation, readback byte-size and command-order
validation, texture-upload byte-count validation, frame-capture byte-size and validation, texture-upload byte-count validation, frame-capture byte-size and
command-order validation, render-target blit validation, texture-slot binding command-order validation, render-target blit validation, texture-slot binding
validation, recording texture-bind/upload/readback/frame-capture/blit command validation, blend-state validation, recording blend/texture-bind/upload/
capture, and invalid catalog rejection. readback/frame-capture/blit command capture, and invalid catalog rejection.
- `pp_paint_renderer_compositor_tests` passed. - `pp_paint_renderer_compositor_tests` passed.
- `pp_ui_core_color_tests` passed. - `pp_ui_core_color_tests` passed.
- `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_value_tests` passed.
@@ -817,15 +817,16 @@ Results:
reintroduces raw `GL_*`/`WGL_*` constants outside the allowed legacy OpenGL reintroduces raw `GL_*`/`WGL_*` constants outside the allowed legacy OpenGL
implementation files. implementation files.
- `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict - `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict
command-order/texture-bind/texture-upload/readback/frame-capture/blit command-order/blend-state/texture-bind/texture-upload/readback/frame-capture/
validation; it records commands, trace markers, texture binds, blit validation; it records commands, trace markers, blend state, texture
uploads/readbacks, frame captures, and render-target blits, giving automation binds, uploads/readbacks, frame captures, and render-target blits, giving
a backend-neutral render path that does not require a window or GL context. 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 - `pano_cli record-render` exercises that headless recording renderer and emits
JSON command counts, target dimensions, backend name, trace/draw summary, and JSON command counts, target dimensions, backend name, trace/draw summary, and
texture-bind/upload/readback/frame-capture/blit command/byte totals for agent blend-state plus texture-bind/upload/readback/frame-capture/blit command/byte
automation, with an expected-failure smoke for oversized render/readback totals for agent automation, with an expected-failure smoke for oversized
targets. render/readback targets.
- `pano_cli simulate-document-history` exercises pure document history - `pano_cli simulate-document-history` exercises pure document history
apply/undo/redo behavior and emits JSON layer/frame/history state for agent apply/undo/redo behavior and emits JSON layer/frame/history state for agent
automation. automation.

View File

@@ -122,6 +122,24 @@ pp::foundation::Status RecordingCommandContext::set_viewport(Viewport viewport)
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
pp::foundation::Status RecordingCommandContext::set_blend_state(BlendState state) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto status = validate_blend_state(state);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::set_blend_state,
.blend_state = state,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::bind_shader(IShaderProgram& shader) noexcept pp::foundation::Status RecordingCommandContext::bind_shader(IShaderProgram& shader) noexcept
{ {
if (!in_render_pass_) { if (!in_render_pass_) {
@@ -400,6 +418,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no
return "begin_render_pass"; return "begin_render_pass";
case RecordedRenderCommandKind::set_viewport: case RecordedRenderCommandKind::set_viewport:
return "set_viewport"; return "set_viewport";
case RecordedRenderCommandKind::set_blend_state:
return "set_blend_state";
case RecordedRenderCommandKind::bind_shader: case RecordedRenderCommandKind::bind_shader:
return "bind_shader"; return "bind_shader";
case RecordedRenderCommandKind::bind_texture: case RecordedRenderCommandKind::bind_texture:

View File

@@ -10,6 +10,7 @@ namespace pp::renderer {
enum class RecordedRenderCommandKind : std::uint8_t { enum class RecordedRenderCommandKind : std::uint8_t {
begin_render_pass, begin_render_pass,
set_viewport, set_viewport,
set_blend_state,
bind_shader, bind_shader,
bind_texture, bind_texture,
bind_mesh, bind_mesh,
@@ -27,6 +28,7 @@ struct RecordedRenderCommand {
TextureDesc target_desc {}; TextureDesc target_desc {};
ClearColor clear_color {}; ClearColor clear_color {};
Viewport viewport {}; Viewport viewport {};
BlendState blend_state {};
MeshDesc mesh_desc {}; MeshDesc mesh_desc {};
TextureDesc texture_desc {}; TextureDesc texture_desc {};
std::uint32_t texture_slot = 0; std::uint32_t texture_slot = 0;
@@ -98,6 +100,7 @@ public:
IRenderTarget& target, IRenderTarget& target,
ClearColor clear_color) noexcept override; ClearColor clear_color) noexcept override;
[[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override; [[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override;
[[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override;
[[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override; [[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override;
[[nodiscard]] pp::foundation::Status bind_texture( [[nodiscard]] pp::foundation::Status bind_texture(
std::uint32_t slot, std::uint32_t slot,

View File

@@ -131,6 +131,63 @@ pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_exte
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept
{
switch (factor) {
case BlendFactor::zero:
case BlendFactor::one:
case BlendFactor::source_alpha:
case BlendFactor::one_minus_source_alpha:
case BlendFactor::destination_alpha:
case BlendFactor::one_minus_destination_alpha:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("blend factor is not supported");
}
pp::foundation::Status validate_blend_op(BlendOp op) noexcept
{
switch (op) {
case BlendOp::add:
case BlendOp::subtract:
case BlendOp::reverse_subtract:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("blend operation is not supported");
}
pp::foundation::Status validate_blend_state(BlendState state) noexcept
{
const auto source_color = validate_blend_factor(state.source_color);
if (!source_color.ok()) {
return source_color;
}
const auto destination_color = validate_blend_factor(state.destination_color);
if (!destination_color.ok()) {
return destination_color;
}
const auto color_op = validate_blend_op(state.color_op);
if (!color_op.ok()) {
return color_op;
}
const auto source_alpha = validate_blend_factor(state.source_alpha);
if (!source_alpha.ok()) {
return source_alpha;
}
const auto destination_alpha = validate_blend_factor(state.destination_alpha);
if (!destination_alpha.ok()) {
return destination_alpha;
}
return validate_blend_op(state.alpha_op);
}
pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept
{ {
if (desc.vertex_count == 0) { if (desc.vertex_count == 0) {
@@ -324,4 +381,38 @@ const char* blit_filter_name(BlitFilter filter) noexcept
return "unknown"; return "unknown";
} }
const char* blend_factor_name(BlendFactor factor) noexcept
{
switch (factor) {
case BlendFactor::zero:
return "zero";
case BlendFactor::one:
return "one";
case BlendFactor::source_alpha:
return "source_alpha";
case BlendFactor::one_minus_source_alpha:
return "one_minus_source_alpha";
case BlendFactor::destination_alpha:
return "destination_alpha";
case BlendFactor::one_minus_destination_alpha:
return "one_minus_destination_alpha";
}
return "unknown";
}
const char* blend_op_name(BlendOp op) noexcept
{
switch (op) {
case BlendOp::add:
return "add";
case BlendOp::subtract:
return "subtract";
case BlendOp::reverse_subtract:
return "reverse_subtract";
}
return "unknown";
}
} }

View File

@@ -65,6 +65,35 @@ enum class BlitFilter : std::uint8_t {
linear, linear,
}; };
enum class BlendFactor : std::uint8_t {
zero,
one,
source_alpha,
one_minus_source_alpha,
destination_alpha,
one_minus_destination_alpha,
};
enum class BlendOp : std::uint8_t {
add,
subtract,
reverse_subtract,
};
struct BlendState {
bool enabled = false;
BlendFactor source_color = BlendFactor::one;
BlendFactor destination_color = BlendFactor::zero;
BlendOp color_op = BlendOp::add;
BlendFactor source_alpha = BlendFactor::one;
BlendFactor destination_alpha = BlendFactor::zero;
BlendOp alpha_op = BlendOp::add;
bool write_r = true;
bool write_g = true;
bool write_b = true;
bool write_a = true;
};
struct MeshDesc { struct MeshDesc {
std::uint32_t vertex_count = 0; std::uint32_t vertex_count = 0;
std::uint32_t index_count = 0; std::uint32_t index_count = 0;
@@ -126,6 +155,7 @@ public:
IRenderTarget& target, IRenderTarget& target,
ClearColor clear_color) noexcept = 0; ClearColor clear_color) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_blend_state(BlendState state) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status bind_texture( [[nodiscard]] virtual pp::foundation::Status bind_texture(
std::uint32_t slot, std::uint32_t slot,
@@ -163,6 +193,9 @@ public:
[[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept; [[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept;
[[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept; [[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept;
[[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept; [[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept;
[[nodiscard]] pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept;
[[nodiscard]] pp::foundation::Status validate_blend_op(BlendOp op) noexcept;
[[nodiscard]] pp::foundation::Status validate_blend_state(BlendState state) noexcept;
[[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept; [[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_texture_slot(std::uint32_t slot) noexcept;
[[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept;
@@ -179,5 +212,7 @@ public:
[[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept; [[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept;
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; [[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
[[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept; [[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept;
[[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept;
[[nodiscard]] const char* blend_op_name(BlendOp op) noexcept;
} }

View File

@@ -365,7 +365,7 @@ if(TARGET pano_cli)
COMMAND pano_cli record-render --width 32 --height 16) COMMAND pano_cli record-render --width 32 --height 16)
set_tests_properties(pano_cli_record_render_smoke PROPERTIES set_tests_properties(pano_cli_record_render_smoke PROPERTIES
LABELS "renderer;integration;desktop-fast" LABELS "renderer;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"commands\":12.*\"drawCommands\":1.*\"bindTextureCommands\":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.*\"commands\":13.*\"drawCommands\":1.*\"blendCommands\":1.*\"bindTextureCommands\":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 add_test(NAME pano_cli_record_render_rejects_oversized_target
COMMAND "${CMAKE_COMMAND}" COMMAND "${CMAKE_COMMAND}"

View File

@@ -10,6 +10,11 @@
using pp::foundation::StatusCode; using pp::foundation::StatusCode;
using pp::renderer::BlitFilter; using pp::renderer::BlitFilter;
using pp::renderer::blit_filter_name; using pp::renderer::blit_filter_name;
using pp::renderer::BlendFactor;
using pp::renderer::blend_factor_name;
using pp::renderer::BlendOp;
using pp::renderer::blend_op_name;
using pp::renderer::BlendState;
using pp::renderer::ClearColor; using pp::renderer::ClearColor;
using pp::renderer::Extent2D; using pp::renderer::Extent2D;
using pp::renderer::frame_capture_byte_size; using pp::renderer::frame_capture_byte_size;
@@ -47,6 +52,9 @@ using pp::renderer::texture_format_name;
using pp::renderer::validate_extent; using pp::renderer::validate_extent;
using pp::renderer::validate_blit_descs; using pp::renderer::validate_blit_descs;
using pp::renderer::validate_blit_filter; using pp::renderer::validate_blit_filter;
using pp::renderer::validate_blend_factor;
using pp::renderer::validate_blend_op;
using pp::renderer::validate_blend_state;
using pp::renderer::validate_mesh_desc; using pp::renderer::validate_mesh_desc;
using pp::renderer::validate_readback_region; using pp::renderer::validate_readback_region;
using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_catalog;
@@ -148,6 +156,19 @@ public:
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
[[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override
{
if (!in_render_pass) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto status = validate_blend_state(state);
if (!status.ok()) {
return status;
}
last_blend_state = state;
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status bind_texture( [[nodiscard]] pp::foundation::Status bind_texture(
std::uint32_t slot, std::uint32_t slot,
pp::renderer::ITexture2D& texture) noexcept override pp::renderer::ITexture2D& texture) noexcept override
@@ -265,6 +286,7 @@ public:
bool in_render_pass = false; bool in_render_pass = false;
const char* shader_name = nullptr; const char* shader_name = nullptr;
BlendState last_blend_state {};
std::uint32_t last_texture_slot = 0; std::uint32_t last_texture_slot = 0;
std::uint64_t last_texture_bytes = 0; std::uint64_t last_texture_bytes = 0;
std::uint64_t last_upload_bytes = 0; std::uint64_t last_upload_bytes = 0;
@@ -437,6 +459,41 @@ void validates_blit_contract(pp::tests::Harness& h)
PP_EXPECT(h, blit_filter_name(static_cast<BlitFilter>(255)) == std::string_view("unknown")); PP_EXPECT(h, blit_filter_name(static_cast<BlitFilter>(255)) == std::string_view("unknown"));
} }
void validates_blend_contract(pp::tests::Harness& h)
{
const BlendState alpha_blend {
.enabled = true,
.source_color = BlendFactor::source_alpha,
.destination_color = BlendFactor::one_minus_source_alpha,
.color_op = BlendOp::add,
.source_alpha = BlendFactor::one,
.destination_alpha = BlendFactor::one_minus_source_alpha,
.alpha_op = BlendOp::add,
};
PP_EXPECT(h, validate_blend_state(alpha_blend).ok());
PP_EXPECT(h, validate_blend_factor(BlendFactor::destination_alpha).ok());
PP_EXPECT(h, validate_blend_op(BlendOp::reverse_subtract).ok());
PP_EXPECT(h, blend_factor_name(BlendFactor::one_minus_destination_alpha)
== std::string_view("one_minus_destination_alpha"));
PP_EXPECT(h, blend_op_name(BlendOp::subtract) == std::string_view("subtract"));
auto bad_source = alpha_blend;
bad_source.source_color = static_cast<BlendFactor>(255);
auto bad_op = alpha_blend;
bad_op.alpha_op = static_cast<BlendOp>(255);
const auto bad_source_status = validate_blend_state(bad_source);
const auto bad_op_status = validate_blend_state(bad_op);
PP_EXPECT(h, !bad_source_status.ok());
PP_EXPECT(h, bad_source_status.code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_op_status.ok());
PP_EXPECT(h, bad_op_status.code == StatusCode::invalid_argument);
PP_EXPECT(h, blend_factor_name(static_cast<BlendFactor>(255)) == std::string_view("unknown"));
PP_EXPECT(h, blend_op_name(static_cast<BlendOp>(255)) == std::string_view("unknown"));
}
void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h) void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h)
{ {
const Extent2D target { .width = 64, .height = 32 }; const Extent2D target { .width = 64, .height = 32 };
@@ -598,6 +655,12 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
auto& context = device.immediate_context(); auto& context = device.immediate_context();
PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }).ok()); PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }).ok());
PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok());
PP_EXPECT(h, context.set_blend_state(BlendState {
.enabled = true,
.source_color = BlendFactor::source_alpha,
.destination_color = BlendFactor::one_minus_source_alpha,
})
.ok());
PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.bind_shader(shader).ok());
PP_EXPECT(h, context.bind_texture(2, texture).ok()); PP_EXPECT(h, context.bind_texture(2, texture).ok());
PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok());
@@ -626,6 +689,9 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
BlitFilter::linear) BlitFilter::linear)
.ok()); .ok());
PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader")); PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader"));
PP_EXPECT(h, device.context.last_blend_state.enabled);
PP_EXPECT(h, device.context.last_blend_state.source_color == BlendFactor::source_alpha);
PP_EXPECT(h, device.context.last_blend_state.destination_color == BlendFactor::one_minus_source_alpha);
PP_EXPECT(h, device.context.last_texture_slot == 2U); PP_EXPECT(h, device.context.last_texture_slot == 2U);
PP_EXPECT(h, device.context.last_texture_bytes == 8192U); PP_EXPECT(h, device.context.last_texture_bytes == 8192U);
PP_EXPECT(h, device.context.last_upload_bytes == 80U); PP_EXPECT(h, device.context.last_upload_bytes == 80U);
@@ -665,6 +731,14 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
auto& context = device.immediate_context(); auto& context = device.immediate_context();
PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }).ok()); PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }).ok());
PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok());
PP_EXPECT(h, context.set_blend_state(BlendState {
.enabled = true,
.source_color = BlendFactor::source_alpha,
.destination_color = BlendFactor::one_minus_source_alpha,
.source_alpha = BlendFactor::one,
.destination_alpha = BlendFactor::one_minus_source_alpha,
})
.ok());
PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.bind_shader(shader).ok());
PP_EXPECT(h, context.bind_texture(1, texture).ok()); PP_EXPECT(h, context.bind_texture(1, texture).ok());
PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok());
@@ -672,7 +746,7 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
context.end_render_pass(); context.end_render_pass();
const auto commands = device.commands(); const auto commands = device.commands();
PP_EXPECT(h, commands.size() == 8U); PP_EXPECT(h, commands.size() == 9U);
PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_marker); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_marker);
PP_EXPECT(h, commands[0].component == std::string_view("renderer")); PP_EXPECT(h, commands[0].component == std::string_view("renderer"));
PP_EXPECT(h, commands[0].name == std::string_view("frame")); PP_EXPECT(h, commands[0].name == std::string_view("frame"));
@@ -681,17 +755,21 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
PP_EXPECT(h, commands[1].clear_color.a == 1.0F); PP_EXPECT(h, commands[1].clear_color.a == 1.0F);
PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_viewport); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_viewport);
PP_EXPECT(h, commands[2].viewport.height == 32U); PP_EXPECT(h, commands[2].viewport.height == 32U);
PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::bind_shader); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::set_blend_state);
PP_EXPECT(h, commands[3].name == std::string_view("recorded-shader")); PP_EXPECT(h, commands[3].blend_state.enabled);
PP_EXPECT(h, commands[4].kind == RecordedRenderCommandKind::bind_texture); PP_EXPECT(h, commands[3].blend_state.destination_color == BlendFactor::one_minus_source_alpha);
PP_EXPECT(h, commands[4].texture_slot == 1U); PP_EXPECT(h, recorded_render_command_kind_name(commands[3].kind) == std::string_view("set_blend_state"));
PP_EXPECT(h, commands[4].texture_desc.extent.height == 32U); PP_EXPECT(h, commands[4].kind == RecordedRenderCommandKind::bind_shader);
PP_EXPECT(h, recorded_render_command_kind_name(commands[4].kind) == std::string_view("bind_texture")); PP_EXPECT(h, commands[4].name == std::string_view("recorded-shader"));
PP_EXPECT(h, commands[5].kind == RecordedRenderCommandKind::bind_mesh); PP_EXPECT(h, commands[5].kind == RecordedRenderCommandKind::bind_texture);
PP_EXPECT(h, commands[5].mesh_desc.vertex_count == 3U); PP_EXPECT(h, commands[5].texture_slot == 1U);
PP_EXPECT(h, commands[6].kind == RecordedRenderCommandKind::draw); PP_EXPECT(h, commands[5].texture_desc.extent.height == 32U);
PP_EXPECT(h, commands[7].kind == RecordedRenderCommandKind::end_render_pass); PP_EXPECT(h, recorded_render_command_kind_name(commands[5].kind) == std::string_view("bind_texture"));
PP_EXPECT(h, recorded_render_command_kind_name(commands[6].kind) == std::string_view("draw")); PP_EXPECT(h, commands[6].kind == RecordedRenderCommandKind::bind_mesh);
PP_EXPECT(h, commands[6].mesh_desc.vertex_count == 3U);
PP_EXPECT(h, commands[7].kind == RecordedRenderCommandKind::draw);
PP_EXPECT(h, commands[8].kind == RecordedRenderCommandKind::end_render_pass);
PP_EXPECT(h, recorded_render_command_kind_name(commands[7].kind) == std::string_view("draw"));
PP_EXPECT(h, context.upload_texture( PP_EXPECT(h, context.upload_texture(
texture, texture,
@@ -699,12 +777,12 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
upload_bytes) upload_bytes)
.ok()); .ok());
const auto commands_after_upload = device.commands(); const auto commands_after_upload = device.commands();
PP_EXPECT(h, commands_after_upload.size() == 9U); PP_EXPECT(h, commands_after_upload.size() == 10U);
PP_EXPECT(h, commands_after_upload[8].kind == RecordedRenderCommandKind::upload_texture); PP_EXPECT(h, commands_after_upload[9].kind == RecordedRenderCommandKind::upload_texture);
PP_EXPECT(h, commands_after_upload[8].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_upload[9].texture_desc.extent.width == 64U);
PP_EXPECT(h, commands_after_upload[8].readback_region.x == 4U); PP_EXPECT(h, commands_after_upload[9].readback_region.x == 4U);
PP_EXPECT(h, commands_after_upload[8].upload_bytes == 96U); PP_EXPECT(h, commands_after_upload[9].upload_bytes == 96U);
PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[8].kind) == std::string_view("upload_texture")); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[9].kind) == std::string_view("upload_texture"));
PP_EXPECT(h, context.read_texture( PP_EXPECT(h, context.read_texture(
texture, texture,
@@ -712,22 +790,22 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
readback_buffer) readback_buffer)
.ok()); .ok());
const auto commands_after_readback = device.commands(); const auto commands_after_readback = device.commands();
PP_EXPECT(h, commands_after_readback.size() == 10U); PP_EXPECT(h, commands_after_readback.size() == 11U);
PP_EXPECT(h, commands_after_readback[9].kind == RecordedRenderCommandKind::read_texture); PP_EXPECT(h, commands_after_readback[10].kind == RecordedRenderCommandKind::read_texture);
PP_EXPECT(h, commands_after_readback[9].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_readback[10].texture_desc.extent.width == 64U);
PP_EXPECT(h, commands_after_readback[9].readback_region.x == 4U); PP_EXPECT(h, commands_after_readback[10].readback_region.x == 4U);
PP_EXPECT(h, commands_after_readback[9].readback_region.height == 3U); PP_EXPECT(h, commands_after_readback[10].readback_region.height == 3U);
PP_EXPECT(h, commands_after_readback[9].readback_bytes == 96U); PP_EXPECT(h, commands_after_readback[10].readback_bytes == 96U);
PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[9].kind) == std::string_view("read_texture")); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[10].kind) == std::string_view("read_texture"));
PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok());
const auto commands_after_capture = device.commands(); const auto commands_after_capture = device.commands();
PP_EXPECT(h, commands_after_capture.size() == 11U); PP_EXPECT(h, commands_after_capture.size() == 12U);
PP_EXPECT(h, commands_after_capture[10].kind == RecordedRenderCommandKind::capture_frame); PP_EXPECT(h, commands_after_capture[11].kind == RecordedRenderCommandKind::capture_frame);
PP_EXPECT(h, commands_after_capture[10].target_desc.extent.width == 64U); PP_EXPECT(h, commands_after_capture[11].target_desc.extent.width == 64U);
PP_EXPECT(h, commands_after_capture[10].target_desc.extent.height == 32U); PP_EXPECT(h, commands_after_capture[11].target_desc.extent.height == 32U);
PP_EXPECT(h, commands_after_capture[10].capture_bytes == 8192U); PP_EXPECT(h, commands_after_capture[11].capture_bytes == 8192U);
PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[10].kind) == std::string_view("capture_frame")); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[11].kind) == std::string_view("capture_frame"));
PP_EXPECT(h, context.blit_render_target( PP_EXPECT(h, context.blit_render_target(
target, target,
@@ -737,16 +815,16 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
BlitFilter::linear) BlitFilter::linear)
.ok()); .ok());
const auto commands_after_blit = device.commands(); const auto commands_after_blit = device.commands();
PP_EXPECT(h, commands_after_blit.size() == 12U); PP_EXPECT(h, commands_after_blit.size() == 13U);
PP_EXPECT(h, commands_after_blit[11].kind == RecordedRenderCommandKind::blit_render_target); PP_EXPECT(h, commands_after_blit[12].kind == RecordedRenderCommandKind::blit_render_target);
PP_EXPECT(h, commands_after_blit[11].source_desc.extent.width == 64U); PP_EXPECT(h, commands_after_blit[12].source_desc.extent.width == 64U);
PP_EXPECT(h, commands_after_blit[11].destination_desc.extent.height == 32U); PP_EXPECT(h, commands_after_blit[12].destination_desc.extent.height == 32U);
PP_EXPECT(h, commands_after_blit[11].source_region.width == 16U); PP_EXPECT(h, commands_after_blit[12].source_region.width == 16U);
PP_EXPECT(h, commands_after_blit[11].destination_region.x == 2U); PP_EXPECT(h, commands_after_blit[12].destination_region.x == 2U);
PP_EXPECT(h, commands_after_blit[11].blit_filter == BlitFilter::linear); PP_EXPECT(h, commands_after_blit[12].blit_filter == BlitFilter::linear);
PP_EXPECT(h, commands_after_blit[11].blit_source_bytes == 512U); PP_EXPECT(h, commands_after_blit[12].blit_source_bytes == 512U);
PP_EXPECT(h, commands_after_blit[11].blit_destination_bytes == 128U); PP_EXPECT(h, commands_after_blit[12].blit_destination_bytes == 128U);
PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[11].kind) == std::string_view("blit_render_target")); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[12].kind) == std::string_view("blit_render_target"));
device.clear(); device.clear();
PP_EXPECT(h, device.commands().empty()); PP_EXPECT(h, device.commands().empty());
@@ -788,6 +866,10 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har
PP_EXPECT(h, !draw_before_begin.ok()); PP_EXPECT(h, !draw_before_begin.ok());
PP_EXPECT(h, draw_before_begin.code == StatusCode::invalid_argument); PP_EXPECT(h, draw_before_begin.code == StatusCode::invalid_argument);
const auto blend_before_begin = context.set_blend_state(BlendState {});
PP_EXPECT(h, !blend_before_begin.ok());
PP_EXPECT(h, blend_before_begin.code == StatusCode::invalid_argument);
const auto invalid_target = context.begin_render_pass(non_render_target, ClearColor {}); const auto invalid_target = context.begin_render_pass(non_render_target, ClearColor {});
PP_EXPECT(h, !invalid_target.ok()); PP_EXPECT(h, !invalid_target.ok());
PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument); PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument);
@@ -825,6 +907,19 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har
PP_EXPECT(h, !nested_begin.ok()); PP_EXPECT(h, !nested_begin.ok());
PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument); PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument);
auto invalid_blend = BlendState {};
invalid_blend.source_color = static_cast<BlendFactor>(255);
const auto invalid_blend_state = context.set_blend_state(invalid_blend);
PP_EXPECT(h, !invalid_blend_state.ok());
PP_EXPECT(h, invalid_blend_state.code == StatusCode::invalid_argument);
PP_EXPECT(h, context.set_blend_state(BlendState {
.enabled = true,
.source_color = BlendFactor::source_alpha,
.destination_color = BlendFactor::one_minus_source_alpha,
})
.ok());
const auto draw_without_bindings = context.draw(); const auto draw_without_bindings = context.draw();
PP_EXPECT(h, !draw_without_bindings.ok()); PP_EXPECT(h, !draw_without_bindings.ok());
PP_EXPECT(h, draw_without_bindings.code == StatusCode::invalid_argument); PP_EXPECT(h, draw_without_bindings.code == StatusCode::invalid_argument);
@@ -851,6 +946,10 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har
PP_EXPECT(h, !viewport_after_end.ok()); PP_EXPECT(h, !viewport_after_end.ok());
PP_EXPECT(h, viewport_after_end.code == StatusCode::invalid_argument); PP_EXPECT(h, viewport_after_end.code == StatusCode::invalid_argument);
const auto blend_after_end = context.set_blend_state(BlendState {});
PP_EXPECT(h, !blend_after_end.ok());
PP_EXPECT(h, blend_after_end.code == StatusCode::invalid_argument);
const auto bind_texture_after_end = context.bind_texture(0, texture); const auto bind_texture_after_end = context.bind_texture(0, texture);
PP_EXPECT(h, !bind_texture_after_end.ok()); PP_EXPECT(h, !bind_texture_after_end.ok());
PP_EXPECT(h, bind_texture_after_end.code == StatusCode::invalid_argument); PP_EXPECT(h, bind_texture_after_end.code == StatusCode::invalid_argument);
@@ -939,6 +1038,7 @@ int main()
harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes); harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes);
harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes); harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes);
harness.run("validates_blit_contract", validates_blit_contract); harness.run("validates_blit_contract", validates_blit_contract);
harness.run("validates_blend_contract", validates_blend_contract);
harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); 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_program_descriptors", validates_shader_program_descriptors);
harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog);

View File

@@ -2273,6 +2273,13 @@ int record_render(int argc, char** argv)
} }
const auto shader_status = context.bind_shader(shader); const auto shader_status = context.bind_shader(shader);
const auto blend_status = context.set_blend_state(pp::renderer::BlendState {
.enabled = true,
.source_color = pp::renderer::BlendFactor::source_alpha,
.destination_color = pp::renderer::BlendFactor::one_minus_source_alpha,
.source_alpha = pp::renderer::BlendFactor::one,
.destination_alpha = pp::renderer::BlendFactor::one_minus_source_alpha,
});
const auto bind_texture_status = context.bind_texture(0, texture); const auto bind_texture_status = context.bind_texture(0, texture);
const auto mesh_status = context.bind_mesh(mesh); const auto mesh_status = context.bind_mesh(mesh);
const auto draw_status = context.draw(); const auto draw_status = context.draw();
@@ -2282,6 +2289,10 @@ int record_render(int argc, char** argv)
print_error("record-render", shader_status.message); print_error("record-render", shader_status.message);
return 2; return 2;
} }
if (!blend_status.ok()) {
print_error("record-render", blend_status.message);
return 2;
}
if (!bind_texture_status.ok()) { if (!bind_texture_status.ok()) {
print_error("record-render", bind_texture_status.message); print_error("record-render", bind_texture_status.message);
return 2; return 2;
@@ -2337,6 +2348,7 @@ int record_render(int argc, char** argv)
} }
std::size_t draw_commands = 0; std::size_t draw_commands = 0;
std::size_t blend_commands = 0;
std::size_t bind_texture_commands = 0; std::size_t bind_texture_commands = 0;
std::size_t upload_commands = 0; std::size_t upload_commands = 0;
std::size_t readback_commands = 0; std::size_t readback_commands = 0;
@@ -2353,6 +2365,8 @@ int record_render(int argc, char** argv)
for (const auto& command : commands) { for (const auto& command : commands) {
if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) { if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) {
++draw_commands; ++draw_commands;
} else if (command.kind == pp::renderer::RecordedRenderCommandKind::set_blend_state) {
++blend_commands;
} else if (command.kind == pp::renderer::RecordedRenderCommandKind::bind_texture) { } else if (command.kind == pp::renderer::RecordedRenderCommandKind::bind_texture) {
++bind_texture_commands; ++bind_texture_commands;
const auto bound_bytes = pp::renderer::texture_byte_size(command.texture_desc); const auto bound_bytes = pp::renderer::texture_byte_size(command.texture_desc);
@@ -2384,6 +2398,7 @@ int record_render(int argc, char** argv)
<< ",\"format\":\"rgba8\"}" << ",\"format\":\"rgba8\"}"
<< ",\"commands\":" << commands.size() << ",\"commands\":" << commands.size()
<< ",\"drawCommands\":" << draw_commands << ",\"drawCommands\":" << draw_commands
<< ",\"blendCommands\":" << blend_commands
<< ",\"bindTextureCommands\":" << bind_texture_commands << ",\"bindTextureCommands\":" << bind_texture_commands
<< ",\"boundTextureBytes\":" << bound_texture_bytes << ",\"boundTextureBytes\":" << bound_texture_bytes
<< ",\"uploadCommands\":" << upload_commands << ",\"uploadCommands\":" << upload_commands