Harbor

Changelog 8f2e9a9756ae

add mask clip ir validation

@sky · 1 month ago · parent 4986b5cecbc4
1 added 6 modified 0 deleted
docs/gpu_intent_api.md +1 -1 modified
229 unchanged lines hidden
230 230
231 231 - `max_color_targets` reports the implemented color attachment count. Vulkan, D3D11, D3D12, and OpenGL currently report `8` because planned IR lowering, render pass/framebuffer arrays, pipeline color-attachment metadata, and backend RTV/FBO/render-pass binding support are implemented across all targets.
232 232 - `max_push_constant_size` reports the implemented push-constant byte limit for the selected backend. Pipeline and compute-shader creation must reject sizes above this limit.
233 - - `Multiple_Render_Targets` and `Indirect_Draws` are implemented for the planned backend path. `Stencil_Clips` remains false until mask-source IR, stencil attachment/state plumbing, backend lowering, and parity proof exist across all backends.
233 + - `Multiple_Render_Targets` and `Indirect_Draws` are implemented for the planned backend path. Mask clip IR exists as a semantic model for geometry coverage rendered into stencil, but `Stencil_Clips` remains false until stencil attachment/state plumbing, backend lowering, and parity proof exist across all backends. Texture-alpha/discard masking does not count as stencil clipping.
234 234
235 235 Public API must not expose Vulkan/D3D-specific terms except through backend-only layers.
236 236
22 unchanged lines hidden
frame_api.odin modified

Diff hidden because this file has more than 800 lines.

render_ir/diagnostics.odin +56 -0 modified
29 unchanged lines hidden
30 30 Invalid_Descriptor,
31 31 Resource_Feedback,
32 32 Access_Metadata_Missing,
33 + Invalid_Mask,
34 + Stencil_Unsupported,
33 35 Internal_Error,
34 36 }
35 37
68 unchanged lines hidden
104 106 case .Invalid_Descriptor: return "RIR0008"
105 107 case .Resource_Feedback: return "RIR0009"
106 108 case .Access_Metadata_Missing: return "RIR0010"
109 + case .Invalid_Mask: return "RIR0011"
110 + case .Stencil_Unsupported: return "RIR0012"
107 111 case .Internal_Error: return "RIR9999"
108 112 }
109 113 return "RIR9999"
60 unchanged lines hidden
170 174 line_num += 1
171 175 }
172 176 }
177 +
173 178 if line_num != d.span.line_start {
174 179 return format_diagnostic(d, allocator)
175 180 }
123 unchanged lines hidden
299 304 add_error(diagnostics, .Invalid_Handle, "clip uses invalid target handle")
300 305 }
301 306 }
307 + if clip.kind == .Mask {
308 + mask, mask_ok := get_mask(ir, clip.mask)
309 + if !mask_ok {
310 + add_error(diagnostics, .Invalid_Mask, "mask clip uses invalid mask handle")
311 + continue
312 + }
313 + if mask.target != clip.target {
314 + add_error(diagnostics, .Invalid_Mask, "mask clip target does not match mask target")
315 + }
316 + }
302 317 }
303 318
319 + for mask in ir.masks {
320 + if _, ok := get_target(ir, mask.target); !ok {
321 + add_error(diagnostics, .Invalid_Mask, "mask uses invalid target handle")
322 + }
323 + switch mask.source_kind {
324 + case .None:
325 + add_error(diagnostics, .Invalid_Mask, "mask requires a source")
326 + case .Geometry_Coverage:
327 + resource, resource_ok := get_resource(ir, mask.coverage_resource)
328 + if !resource_ok || resource.kind != .Buffer {
329 + add_error(diagnostics, .Invalid_Mask, "mask geometry coverage requires a buffer resource")
330 + } else if .Vertex not_in resource.buffer.usage {
331 + add_error(diagnostics, .Invalid_Mask, "mask geometry coverage buffer lacks Vertex usage")
332 + }
333 + if mask.vertex_count == 0 && mask.index_count == 0 {
334 + add_error(diagnostics, .Invalid_Mask, "mask geometry coverage has no vertices or indices")
335 + }
336 + if mask.index_resource != INVALID_RESOURCE {
337 + index_resource, index_ok := get_resource(ir, mask.index_resource)
338 + if !index_ok || index_resource.kind != .Buffer {
339 + add_error(diagnostics, .Invalid_Mask, "mask geometry index source requires a buffer resource")
340 + } else if .Index not_in index_resource.buffer.usage {
341 + add_error(diagnostics, .Invalid_Mask, "mask geometry index buffer lacks Index usage")
342 + }
343 + }
344 + }
345 + }
346 +
304 347 for draw in ir.draw_commands {
305 348 if !draw_kind_is_packet(draw.kind) {
306 349 continue
11 unchanged lines hidden
318 361 add_error(diagnostics, .Invalid_Handle, "draw packet uses invalid clip handle")
319 362 }
320 363 }
364 +
321 365 if packet.material != INVALID_MATERIAL {
322 366 if _, ok := get_material(ir, packet.material); !ok {
323 367 add_error(diagnostics, .Invalid_Handle, "draw packet uses invalid material handle")
324 368 }
325 369 }
370 +
326 371 if packet.geometry.resource != INVALID_RESOURCE {
327 372 if _, ok := get_resource(ir, packet.geometry.resource); !ok {
328 373 add_error(diagnostics, .Invalid_Resource, "draw packet geometry uses invalid resource handle")
45 unchanged lines hidden
374 419 }
375 420 }
376 421 }
422 +
423 + validate_mask_clips_supported :: proc(ir: ^Frame_IR, diagnostics: ^Diagnostic_List, stencil_clips_supported: bool) {
424 + if stencil_clips_supported {
425 + return
426 + }
427 + for clip in ir.clips {
428 + if clip.kind == .Mask {
429 + add_error(diagnostics, .Stencil_Unsupported, "mask clips require backend Stencil_Clips support")
430 + }
431 + }
432 + }
render_ir/dump.odin modified

Diff hidden because this file has more than 800 lines.

render_ir/render_ir.odin modified

Diff hidden because this file has more than 800 lines.

tests/render_ir/diagnostics_test.odin +2 -0 modified
15 unchanged lines hidden
16 16 testing.expect_value(t, ir.format_code(.Invalid_Descriptor), "RIR0008")
17 17 testing.expect_value(t, ir.format_code(.Resource_Feedback), "RIR0009")
18 18 testing.expect_value(t, ir.format_code(.Access_Metadata_Missing), "RIR0010")
19 + testing.expect_value(t, ir.format_code(.Invalid_Mask), "RIR0011")
20 + testing.expect_value(t, ir.format_code(.Stencil_Unsupported), "RIR0012")
19 21 testing.expect_value(t, ir.format_code(.Internal_Error), "RIR9999")
20 22 }
21 23
94 unchanged lines hidden
tests/render_ir/mask_clip_test.odin +133 -0 added
1 + package render_ir_tests
2 +
3 + import ir "../../render_ir"
4 + import "core:testing"
5 +
6 + make_mask_frame :: proc() -> (
7 + frame: ir.Frame_IR,
8 + target: ir.Target_Handle,
9 + coverage: ir.Resource_Handle,
10 + ) {
11 + frame = ir.init_frame_ir()
12 + target = ir.add_target(
13 + &frame,
14 + {
15 + kind = .Present,
16 + name = "present",
17 + width = 256,
18 + height = 256,
19 + color_format = .B8G8R8A8_SRGB,
20 + clear_color = {0, 0, 0, 1},
21 + clear_depth = 1,
22 + },
23 + )
24 + coverage = ir.add_buffer(&frame, "mask-coverage", 256, {.Vertex}, {.Host_Visible}, .Transient)
25 + return
26 + }
27 +
28 + @(test)
29 + test_mask_clip_model_validates_semantic_handles :: proc(t: ^testing.T) {
30 + frame, target, coverage := make_mask_frame()
31 + defer ir.destroy_frame_ir(&frame)
32 +
33 + mask := ir.add_mask(
34 + &frame,
35 + {
36 + target = target,
37 + name = "round-coverage",
38 + source_kind = .Geometry_Coverage,
39 + coverage_resource = coverage,
40 + vertex_count = 6,
41 + },
42 + )
43 + clip := ir.add_clip(&frame, {target = target, kind = .Mask, mask = mask})
44 +
45 + testing.expect(t, mask != ir.INVALID_MASK)
46 + testing.expect(t, clip != ir.INVALID_CLIP)
47 + testing.expect_value(t, ir.mask_count(&frame), 1)
48 + testing.expect_value(t, ir.clip_count(&frame), 1)
49 +
50 + diagnostics := ir.init_diagnostics()
51 + defer ir.destroy_diagnostics(&diagnostics)
52 + ir.validate_intent_handles(&frame, &diagnostics)
53 + testing.expect_value(t, len(diagnostics.items), 0)
54 + }
55 +
56 + @(test)
57 + test_mask_clip_rejects_invalid_mask_handle :: proc(t: ^testing.T) {
58 + frame, target, _ := make_mask_frame()
59 + defer ir.destroy_frame_ir(&frame)
60 +
61 + _ = ir.add_clip(&frame, {target = target, kind = .Mask, mask = ir.Mask_Handle(99)})
62 +
63 + diagnostics := ir.init_diagnostics()
64 + defer ir.destroy_diagnostics(&diagnostics)
65 + ir.validate_intent_handles(&frame, &diagnostics)
66 + testing.expect(t, ir.has_errors(&diagnostics))
67 + testing.expect_value(t, diagnostics.items[0].code, ir.Diagnostic_Code.Invalid_Mask)
68 + }
69 +
70 + @(test)
71 + test_mask_clip_rejects_target_mismatch :: proc(t: ^testing.T) {
72 + frame, target_a, coverage := make_mask_frame()
73 + defer ir.destroy_frame_ir(&frame)
74 + target_b := ir.add_target(
75 + &frame,
76 + {
77 + kind = .Present,
78 + name = "other",
79 + width = 256,
80 + height = 256,
81 + color_format = .B8G8R8A8_SRGB,
82 + clear_color = {0, 0, 0, 1},
83 + clear_depth = 1,
84 + },
85 + )
86 + mask := ir.add_mask(
87 + &frame,
88 + {target = target_a, name = "mask", source_kind = .Geometry_Coverage, coverage_resource = coverage, vertex_count = 3},
89 + )
90 + _ = ir.add_clip(&frame, {target = target_b, kind = .Mask, mask = mask})
91 +
92 + diagnostics := ir.init_diagnostics()
93 + defer ir.destroy_diagnostics(&diagnostics)
94 + ir.validate_intent_handles(&frame, &diagnostics)
95 + testing.expect(t, ir.has_errors(&diagnostics))
96 + testing.expect_value(t, diagnostics.items[0].code, ir.Diagnostic_Code.Invalid_Mask)
97 + }
98 +
99 + @(test)
100 + test_mask_clip_rejects_missing_source :: proc(t: ^testing.T) {
101 + frame, target, _ := make_mask_frame()
102 + defer ir.destroy_frame_ir(&frame)
103 +
104 + _ = ir.add_mask(&frame, {target = target, name = "empty"})
105 +
106 + diagnostics := ir.init_diagnostics()
107 + defer ir.destroy_diagnostics(&diagnostics)
108 + ir.validate_intent_handles(&frame, &diagnostics)
109 + testing.expect(t, ir.has_errors(&diagnostics))
110 + testing.expect_value(t, diagnostics.items[0].code, ir.Diagnostic_Code.Invalid_Mask)
111 + }
112 +
113 + @(test)
114 + test_mask_clip_lowering_refuses_until_stencil_capability_exists :: proc(t: ^testing.T) {
115 + frame, target, coverage := make_mask_frame()
116 + defer ir.destroy_frame_ir(&frame)
117 +
118 + mask := ir.add_mask(
119 + &frame,
120 + {target = target, name = "mask", source_kind = .Geometry_Coverage, coverage_resource = coverage, vertex_count = 3},
121 + )
122 + _ = ir.add_clip(&frame, {target = target, kind = .Mask, mask = mask})
123 +
124 + diagnostics := ir.init_diagnostics()
125 + defer ir.destroy_diagnostics(&diagnostics)
126 + ir.validate_mask_clips_supported(&frame, &diagnostics, false)
127 + testing.expect(t, ir.has_errors(&diagnostics))
128 + testing.expect_value(t, diagnostics.items[0].code, ir.Diagnostic_Code.Stencil_Unsupported)
129 +
130 + ir.reset_diagnostics(&diagnostics)
131 + ir.validate_mask_clips_supported(&frame, &diagnostics, true)
132 + testing.expect(t, !ir.has_errors(&diagnostics))
133 + }