Harbor

Changelog 86790d313997

pin gpu proof substrate

@sky · 1 month ago · parent 31f6aa3e1475
2 added 11 modified 0 deleted
gpu/backend/backend.odin +7 -0 modified
394 unchanged lines hidden
395 395 usage: Image_Usage_Flags,
396 396 }
397 397
398 + Readback_Texture_Desc :: struct {
399 + texture: Texture_Handle,
400 + width, height: u32,
401 + current_layout: Image_Layout,
402 + }
403 +
398 404 Sampler_Desc :: struct {
399 405 mag_filter: Filter,
400 406 min_filter: Filter,
140 unchanged lines hidden
541 547 bool,
542 548 ),
543 549 destroy_texture: proc(handle: Texture_Handle),
550 + read_texture_rgba8: proc(desc: Readback_Texture_Desc, out: []u8) -> bool,
544 551
545 552 // --- Samplers ---
546 553 create_sampler: proc(desc: Sampler_Desc) -> (Sampler_Handle, bool),
179 unchanged lines hidden
gpu/backend/vulkan/vk_backend.odin +1 -0 modified
333 unchanged lines hidden
334 334 // Textures
335 335 create_texture = create_texture_vk,
336 336 destroy_texture = destroy_texture_vk,
337 + read_texture_rgba8 = read_texture_rgba8_vk,
337 338
338 339 // Samplers
339 340 create_sampler = create_sampler_vk,
206 unchanged lines hidden
gpu/backend/vulkan/vk_ops.odin modified

Diff hidden because this file has more than 800 lines.

gpu/docs/gpu_intent_api.md +7 -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
19 unchanged lines hidden
256 256 - invalid clips/targets do not silently cull
257 257 - particle-specific semantic draw kinds are absent from final core IR
258 258 - Vulkan/D3D11/D3D12/OpenGL checks cover backend-neutral lowering
259 +
260 + Visual/runtime proof:
261 +
262 + - Final proof artifacts must come from deterministic readback images, not window screenshots.
263 + - The proof image contract is RGBA8, fixed dimensions, top-left assertion coordinates, straight alpha, and exact byte colors unless a test explicitly declares tolerance.
264 + - Window screenshots are smoke artifacts only and must not close backend-parity ledger rows.
gpu/frame_api.odin modified

Diff hidden because this file has more than 800 lines.

gpu/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 + }
gpu/render_ir/dump.odin modified

Diff hidden because this file has more than 800 lines.

gpu/render_ir/render_ir.odin modified

Diff hidden because this file has more than 800 lines.

gpu/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
gpu/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 + }
gpu/tools/test.lua +1 -0 modified
329 unchanged lines hidden
330 330 assert(process.parse_elapsed_seconds("0:00.15") == 0.15)
331 331 assert(process.parse_elapsed_seconds("1:02:03") == 3723)
332 332 assert(process.command_exists("python3"))
333 + assert(process.run("python3 " .. path.shell_quote(path.join(root, "tools", "visual_assert.py")) .. " --selftest") == 0)
333 334 visual_matrix.selftest(root)
334 335 print("lua selftest ok")
335 336 end
76 unchanged lines hidden
gpu/tools/visual_assert.py +279 -0 added
1 + #!/usr/bin/env python3
2 + import argparse
3 + import json
4 + import os
5 + import struct
6 + import subprocess
7 + import sys
8 + import tempfile
9 +
10 +
11 + PALETTE = {
12 + "background": [16, 16, 24, 255],
13 + "foreground": [240, 48, 96, 255],
14 + "compute_to_draw": [48, 224, 128, 255],
15 + "offscreen_sampled": [64, 128, 240, 255],
16 + "mrt": [240, 64, 64, 255],
17 + "indirect_instance": [64, 240, 64, 255],
18 + "offscreen": [64, 64, 240, 255],
19 + }
20 +
21 +
22 + def _read_token(f):
23 + token = bytearray()
24 + while True:
25 + c = f.read(1)
26 + if not c:
27 + return None
28 + if c == b"#":
29 + f.readline()
30 + continue
31 + if not c.isspace():
32 + token.extend(c)
33 + break
34 + while True:
35 + c = f.read(1)
36 + if not c or c.isspace():
37 + break
38 + token.extend(c)
39 + return token.decode("ascii")
40 +
41 +
42 + def load_ppm(path):
43 + with open(path, "rb") as f:
44 + magic = _read_token(f)
45 + if magic != "P6":
46 + raise ValueError("unsupported PPM magic")
47 + width = int(_read_token(f))
48 + height = int(_read_token(f))
49 + max_value = int(_read_token(f))
50 + if max_value != 255:
51 + raise ValueError("unsupported PPM max value")
52 + data = f.read(width * height * 3)
53 + if len(data) != width * height * 3:
54 + raise ValueError("truncated PPM image")
55 + rgba = bytearray(width * height * 4)
56 + for i in range(width * height):
57 + rgba[i * 4:i * 4 + 3] = data[i * 3:i * 3 + 3]
58 + rgba[i * 4 + 3] = 255
59 + return width, height, bytes(rgba)
60 +
61 +
62 + def load_with_pillow(path):
63 + from PIL import Image
64 + img = Image.open(path).convert("RGBA")
65 + return img.width, img.height, img.tobytes()
66 +
67 +
68 + def load_with_magick(path):
69 + result = subprocess.run(
70 + ["magick", path, "rgba:-"],
71 + stdout=subprocess.PIPE,
72 + stderr=subprocess.PIPE,
73 + )
74 + if result.returncode != 0:
75 + raise ValueError(result.stderr.decode("utf-8", "replace").strip() or "magick failed")
76 + size = subprocess.run(
77 + ["magick", "identify", "-format", "%w %h", path],
78 + stdout=subprocess.PIPE,
79 + stderr=subprocess.PIPE,
80 + text=True,
81 + )
82 + if size.returncode != 0:
83 + raise ValueError(size.stderr.strip() or "magick identify failed")
84 + width, height = [int(v) for v in size.stdout.split()]
85 + return width, height, result.stdout
86 +
87 +
88 + def load_image(path):
89 + lower = path.lower()
90 + if lower.endswith(".ppm"):
91 + return load_ppm(path)
92 + try:
93 + return load_with_pillow(path)
94 + except Exception:
95 + return load_with_magick(path)
96 +
97 +
98 + def color_for(value):
99 + if isinstance(value, str):
100 + if value not in PALETTE:
101 + raise ValueError("unknown palette color: " + value)
102 + return PALETTE[value]
103 + if isinstance(value, list) and len(value) in (3, 4):
104 + out = [int(v) for v in value]
105 + if len(out) == 3:
106 + out.append(255)
107 + return out
108 + raise ValueError("invalid expected color")
109 +
110 +
111 + def pixel_at(width, data, x, y):
112 + idx = (y * width + x) * 4
113 + return list(data[idx:idx + 4])
114 +
115 +
116 + def within(actual, expected, tolerance):
117 + return all(abs(int(a) - int(e)) <= tolerance for a, e in zip(actual, expected))
118 +
119 +
120 + def evaluate_assertion(width, height, data, assertion):
121 + kind = assertion.get("kind")
122 + expected = color_for(assertion.get("expected"))
123 + tolerance = int(assertion.get("tolerance", 0))
124 + result = {
125 + "kind": kind,
126 + "expected": expected,
127 + "tolerance": tolerance,
128 + "pass": False,
129 + }
130 + if kind == "point":
131 + x = int(assertion["x"])
132 + y = int(assertion["y"])
133 + result["x"] = x
134 + result["y"] = y
135 + if x < 0 or y < 0 or x >= width or y >= height:
136 + result["reason"] = "point outside image"
137 + return result
138 + actual = pixel_at(width, data, x, y)
139 + result["actual"] = actual
140 + result["pass"] = within(actual, expected, tolerance)
141 + if not result["pass"]:
142 + result["reason"] = "point color mismatch"
143 + return result
144 + if kind == "rect":
145 + x = int(assertion["x"])
146 + y = int(assertion["y"])
147 + w = int(assertion["w"])
148 + h = int(assertion["h"])
149 + result.update({"x": x, "y": y, "w": w, "h": h})
150 + if x < 0 or y < 0 or w <= 0 or h <= 0 or x + w > width or y + h > height:
151 + result["reason"] = "rect outside image"
152 + return result
153 + mismatches = []
154 + for yy in range(y, y + h):
155 + for xx in range(x, x + w):
156 + actual = pixel_at(width, data, xx, yy)
157 + if not within(actual, expected, tolerance):
158 + mismatches.append({"x": xx, "y": yy, "actual": actual})
159 + if len(mismatches) >= 8:
160 + break
161 + if len(mismatches) >= 8:
162 + break
163 + result["mismatches"] = mismatches
164 + result["pass"] = len(mismatches) == 0
165 + if not result["pass"]:
166 + result["reason"] = "rect color mismatch"
167 + return result
168 + result["reason"] = "unknown assertion kind"
169 + return result
170 +
171 +
172 + def evaluate(path, spec):
173 + if spec.get("source_kind") == "screenshot":
174 + return {
175 + "ok": False,
176 + "path": path,
177 + "source_kind": "screenshot",
178 + "error": "screenshots are smoke artifacts, not proof artifacts",
179 + }
180 + width, height, data = load_image(path)
181 + expected_width = spec.get("width")
182 + expected_height = spec.get("height")
183 + results = []
184 + ok = True
185 + if expected_width is not None and int(expected_width) != width:
186 + ok = False
187 + results.append({"kind": "dimensions", "pass": False, "actual": [width, height], "expected": [expected_width, expected_height]})
188 + if expected_height is not None and int(expected_height) != height:
189 + ok = False
190 + if not results or results[-1].get("kind") != "dimensions":
191 + results.append({"kind": "dimensions", "pass": False, "actual": [width, height], "expected": [expected_width, expected_height]})
192 + for assertion in spec.get("assertions", []):
193 + item = evaluate_assertion(width, height, data, assertion)
194 + results.append(item)
195 + ok = ok and item["pass"]
196 + return {
197 + "ok": ok,
198 + "path": path,
199 + "source_kind": spec.get("source_kind", "readback"),
200 + "origin": spec.get("origin", "top-left"),
201 + "format": "RGBA8",
202 + "width": width,
203 + "height": height,
204 + "assertions": results,
205 + }
206 +
207 +
208 + def write_ppm(path, width, height, fill):
209 + rgb = bytes(fill[:3])
210 + with open(path, "wb") as f:
211 + f.write(("P6\n%d %d\n255\n" % (width, height)).encode("ascii"))
212 + f.write(rgb * width * height)
213 +
214 +
215 + def patch_rect_ppm(path, width, x, y, w, h, color):
216 + with open(path, "r+b") as f:
217 + data = f.read()
218 + header_end = data.find(b"\n255\n") + 5
219 + pixels = bytearray(data[header_end:])
220 + for yy in range(y, y + h):
221 + for xx in range(x, x + w):
222 + idx = (yy * width + xx) * 3
223 + pixels[idx:idx + 3] = bytes(color[:3])
224 + f.seek(header_end)
225 + f.write(pixels)
226 +
227 +
228 + def selftest():
229 + with tempfile.TemporaryDirectory() as tmp:
230 + on = os.path.join(tmp, "feature_on.ppm")
231 + off = os.path.join(tmp, "feature_off.ppm")
232 + write_ppm(on, 4, 4, PALETTE["background"])
233 + write_ppm(off, 4, 4, PALETTE["background"])
234 + patch_rect_ppm(on, 4, 1, 1, 2, 2, PALETTE["foreground"])
235 + spec = {
236 + "source_kind": "fixture",
237 + "width": 4,
238 + "height": 4,
239 + "origin": "top-left",
240 + "assertions": [
241 + {"kind": "point", "x": 1, "y": 1, "expected": "foreground"},
242 + {"kind": "rect", "x": 0, "y": 0, "w": 1, "h": 4, "expected": "background"},
243 + ],
244 + }
245 + passed = evaluate(on, spec)
246 + failed = evaluate(off, spec)
247 + screenshot = evaluate(on, {"source_kind": "screenshot", "assertions": []})
248 + assert passed["ok"], passed
249 + assert not failed["ok"], failed
250 + assert not screenshot["ok"], screenshot
251 + print("visual_assert selftest ok")
252 + return 0
253 +
254 +
255 + def main():
256 + parser = argparse.ArgumentParser()
257 + parser.add_argument("image", nargs="?")
258 + parser.add_argument("--spec", help="JSON assertion spec path")
259 + parser.add_argument("--json", dest="json_path")
260 + parser.add_argument("--selftest", action="store_true")
261 + args = parser.parse_args()
262 + if args.selftest:
263 + return selftest()
264 + if not args.image or not args.spec:
265 + parser.error("image and --spec are required unless --selftest is used")
266 + with open(args.spec, "r", encoding="utf-8") as f:
267 + spec = json.load(f)
268 + result = evaluate(args.image, spec)
269 + encoded = json.dumps(result, sort_keys=True)
270 + if args.json_path:
271 + with open(args.json_path, "w", encoding="utf-8") as f:
272 + f.write(encoded + "\n")
273 + else:
274 + print(encoded)
275 + return 0 if result.get("ok") else 1
276 +
277 +
278 + if __name__ == "__main__":
279 + sys.exit(main())
gpu/tools/visual_matrix.lua +60 -1 modified
21 unchanged lines hidden
22 22 example = "parity_stencil_clip",
23 23 ledger = "bp-stencil",
24 24 negative_control = true,
25 + required_backends = { "vulkan", "opengl", "d3d11", "d3d12" },
25 26 assertions = {
26 27 { kind = "point", x = 48, y = 48, expected = "background" },
27 28 { kind = "point", x = 128, y = 128, expected = "foreground" },
5 unchanged lines hidden
33 34 example = "parity_resource_sync",
34 35 ledger = "bp-resource-sync",
35 36 negative_control = false,
37 + required_backends = { "vulkan", "opengl", "d3d11", "d3d12" },
36 38 assertions = {
37 39 { kind = "point", x = 96, y = 96, expected = "compute_to_draw" },
38 40 { kind = "point", x = 160, y = 96, expected = "offscreen_sampled" },
4 unchanged lines hidden
43 45 example = "parity_backend_features",
44 46 ledger = "bp-validation",
45 47 negative_control = false,
48 + required_backends = { "vulkan", "opengl", "d3d11", "d3d12" },
46 49 assertions = {
47 50 { kind = "point", x = 64, y = 64, expected = "mrt" },
48 51 { kind = "point", x = 128, y = 64, expected = "indirect_instance" },
90 unchanged lines hidden
139 142 raw_result = path.join(dir, "run.json"),
140 143 assertions = scene.assertions,
141 144 negative_control_required = scene.negative_control,
145 + required_backends = scene.required_backends,
142 146 dry_run = opts.dry_run and true or false,
143 147 }
144 148 end
35 unchanged lines hidden
180 184 end
181 185
182 186 row.status = "fail"
183 - row.error = "exact assertion evaluator/readback proof not implemented yet"
187 + row.error = "trusted GPU readback proof artifact missing"
184 188 return row
185 189 end
186 190
11 unchanged lines hidden
198 202 return summary
199 203 end
200 204
205 + local function backend_required(scene, backend)
206 + for _, required in ipairs(scene.required_backends or {}) do
207 + if required == backend then return true end
208 + end
209 + return false
210 + end
211 +
212 + local function ledger_gate(scene, rows)
213 + local gate = {
214 + ledger = scene.ledger,
215 + scene = scene.id,
216 + required_backends = scene.required_backends or {},
217 + status = "incomplete",
218 + pass = {},
219 + fail = {},
220 + skip = {},
221 + missing = {},
222 + }
223 + for _, backend in ipairs(gate.required_backends) do
224 + local found = false
225 + for _, row in ipairs(rows) do
226 + if row.scene == scene.id and row.backend == backend then
227 + found = true
228 + if row.status == "pass" then
229 + gate.pass[#gate.pass + 1] = backend
230 + elseif row.status == "skip" then
231 + gate.skip[#gate.skip + 1] = backend
232 + else
233 + gate.fail[#gate.fail + 1] = backend
234 + end
235 + break
236 + end
237 + end
238 + if not found then
239 + gate.missing[#gate.missing + 1] = backend
240 + end
241 + end
242 + if #gate.pass == #gate.required_backends and #gate.fail == 0 and #gate.skip == 0 and #gate.missing == 0 then
243 + gate.status = "pass"
244 + end
245 + return gate
246 + end
247 +
248 + local function ledger_gates(rows)
249 + local gates = {}
250 + for _, scene in ipairs(SCENES) do
251 + gates[#gates + 1] = ledger_gate(scene, rows)
252 + end
253 + return gates
254 + end
255 +
201 256 function M.run(opts, config)
202 257 config = config or {}
203 258 local root = config.root or path.repo_root("tools/test.lua")
8 unchanged lines hidden
212 267 version = 1,
213 268 rows = rows,
214 269 summary = summarize(rows),
270 + ledger_gates = ledger_gates(rows),
215 271 }
216 272 if opts.json then
217 273 path.mkdir_p(path.dirname(opts.json))
14 unchanged lines hidden
232 288 assert(result.summary.skip == 2)
233 289 assert(result.summary.pass == 0)
234 290 assert(result.summary.fail == 0)
291 + assert(result.ledger_gates[1].status == "incomplete")
292 + assert(#result.ledger_gates[1].skip == 2)
293 + assert(#result.ledger_gates[1].missing == 2)
235 294 assert(result.rows[1].status == "skip")
236 295 assert(result.rows[1].skip_reason == "dry-run")
237 296 assert(result.rows[1].negative_control_required == true)
14 unchanged lines hidden