| … | 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 | ||
Diff hidden because this file has more than 800 lines.
| … | 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 | + | } | |
Diff hidden because this file has more than 800 lines.
Diff hidden because this file has more than 800 lines.
| … | 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 | ||
| 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 | + | } |