Harbor

Changelog 58bd1833ee69

pin gpu resource sync diagnostics

@sky · 1 month ago · parent 138e04c61af8
2 added 12 modified 0 deleted
gpu/backend/d3d11/d3d11_ops.odin modified

Diff hidden because this file has more than 800 lines.

gpu/backend/d3d12/d3d12_backend.odin modified

Diff hidden because this file has more than 800 lines.

gpu/backend/d3d12/d3d12_frame.odin +4 -11 modified
153 unchanged lines hidden
154 154
155 155 // Transition backbuffer to PRESENT
156 156 bb := g_d3d.backbuffers[g_d3d.back_buffer_idx]
157 - transition_resource(bb, {.RENDER_TARGET}, d3d12.RESOURCE_STATE_PRESENT)
157 + transition_backbuffer_to_present_d3d12(bb)
158 158
159 159 // Close command list
160 160 result := g_d3d.command_list->Close()
74 unchanged lines hidden
235 235 continue
236 236 }
237 237 if tex_entry, tex_ok := texture_entry(color_tex); tex_ok && tex_entry.has_rtv {
238 - if tex_entry.state != {.RENDER_TARGET} {
239 - transition_resource(tex_entry.resource, tex_entry.state, {.RENDER_TARGET})
240 - tex_entry.state = {.RENDER_TARGET}
241 - }
238 + transition_texture_to_render_target_d3d12(tex_entry)
242 239 rtv_handles[rtv_count] = get_rtv_cpu_handle(tex_entry.rtv_index)
243 240 rtv_count += 1
244 241 }
4 unchanged lines hidden
249 246 has_dsv := false
250 247 if fb_entry.depth_tex != bk.NULL_TEXTURE {
251 248 if tex_entry, tex_ok := texture_entry(fb_entry.depth_tex); tex_ok && tex_entry.has_dsv {
252 - // Transition depth to DEPTH_WRITE
253 - if tex_entry.state != {.DEPTH_WRITE} {
254 - transition_resource(tex_entry.resource, tex_entry.state, {.DEPTH_WRITE})
255 - tex_entry.state = {.DEPTH_WRITE}
256 - }
249 + transition_texture_to_depth_write_d3d12(tex_entry)
257 250 dsv_handle = get_dsv_cpu_handle(tex_entry.dsv_index)
258 251 has_dsv = true
259 252 }
47 unchanged lines hidden
307 300
308 301 // Transition backbuffer to RENDER_TARGET
309 302 bb := g_d3d.backbuffers[g_d3d.back_buffer_idx]
310 - transition_resource(bb, d3d12.RESOURCE_STATE_PRESENT, {.RENDER_TARGET})
303 + transition_backbuffer_to_render_target_d3d12(bb)
311 304
312 305 // Get RTV for current backbuffer
313 306 rtv_handle := get_rtv_cpu_handle(g_d3d.back_buffer_idx)
82 unchanged lines hidden
gpu/backend/d3d12/d3d12_ops.odin modified

Diff hidden because this file has more than 800 lines.

gpu/backend/opengl/gl_backend.odin +1 -0 modified
44 unchanged lines hidden
45 45 id: u32,
46 46 size: u64,
47 47 usage: bk.Buffer_Usage_Flags,
48 + memory: bk.Memory_Property_Flags,
48 49 mapped_ptr: rawptr,
49 50 active: bool,
50 51 }
261 unchanged lines hidden
gpu/backend/opengl/gl_ops.odin modified

Diff hidden because this file has more than 800 lines.

gpu/backend/vulkan/vk_backend.odin +1 -0 modified
54 unchanged lines hidden
55 55 // Pool entry types
56 56 Vk_Buffer_Entry :: struct {
57 57 buffer: gpu.Gpu_Buffer,
58 + memory: bk.Memory_Property_Flags,
58 59 active: bool,
59 60 }
60 61
484 unchanged lines hidden
gpu/backend/vulkan/vk_ops.odin modified

Diff hidden because this file has more than 800 lines.

gpu/compiler/executor.odin modified

Diff hidden because this file has more than 800 lines.

gpu/docs/gpu_intent_api.md +5 -3 modified
49 unchanged lines hidden
50 50
51 51 CPU mapping is only valid for host-visible buffers. GPU-local/default buffers are not mappable through `map_buffer` or `get_buffer_mapped`; backends must report that explicitly and return `nil`. Host-visible upload buffers may stay persistently mapped when the backend memory model allows it.
52 52
53 - `compute_barrier(ctx)` is a conservative/global **in-frame** barrier for compute-written storage/UAV data consumed by later backend work in the same frame. Its consumer scope is later compute shader reads/writes, vertex/fragment shader reads, vertex/index fetch, and indirect command reads. It is intentionally over-synchronized for correctness; it is not a render-target transition substitute, CPU readback fence, cross-frame lifetime rule, or feedback-loop validator. Backends that lack a complete storage descriptor path must report that separately through capabilities or diagnostics instead of pretending the barrier enables unsupported binding.
53 + `compute_barrier(ctx)` is a conservative/global **in-frame** barrier for compute-written storage/UAV data consumed by later backend work in the same frame. Its consumer scope is later compute shader reads/writes, vertex/fragment shader reads, sampled texture reads, vertex/index/instance fetch, and indirect command reads. It is intentionally over-synchronized for correctness; it is not a render-target transition substitute, CPU readback fence, cross-frame lifetime rule, or feedback-loop validator. Backends that lack a complete storage descriptor path must report that separately through capabilities or diagnostics instead of pretending the barrier enables unsupported binding.
54 54
55 + Resource access diagnostics use backend-neutral access categories: color/depth attachment write, sampled texture read, storage read/write, vertex/index/instance stream read, indirect argument read, copy/upload write, and CPU map/read/write. Permanent invariant: a command must not sample a concrete `Resource_Handle` that is also bound as a render target in the same pass. Current limitation: sampled-input feedback diagnostics are exact only when sampled inputs are represented as `Resource_Handle`s; descriptor/material paths that only carry runtime texture IDs are marked as missing access metadata until those bindings become resource-backed. Current limitation: the runtime treats the default present pass as final, so offscreen work after present is rejected with a command diagnostic rather than silently reordered.
56 +
55 57 Indirect draw argument buffers use the backend-neutral `Indirect_Draw_Args` and `Indirect_Draw_Indexed_Args` ABI. The layouts are intentionally byte-identical to Vulkan, D3D11, D3D12, and OpenGL single-command indirect arguments: 16 bytes for non-indexed draws and 20 bytes for indexed draws. In IR, `packet.instances.indirect` is the argument-buffer resource, `packet.instances.offset` is the byte offset, `packet.instances.stride` is zero-or-ABI-size for indirect arguments, and `packet.instances.count` is limited to one command in the current executor.
56 58
57 59 Per-instance vertex streams use the normal `packet.instances.resource` field, not the indirect argument field. A planned instance-buffer draw requires a graphics pipeline with `has_instance_layout = true`, a nonzero `packet.instances.stride`, and a prepared instance buffer. Slot 0 is the per-vertex stream; slot 1 is the per-instance stream bound with `packet.instances.offset` and `packet.instances.stride`. Vulkan, D3D11, D3D12, and OpenGL all lower the same backend-neutral `Vertex_Binding.input_rate` and `Vertex_Attribute.binding` metadata; OpenGL uses the DSA vertex-attrib-binding/divisor path internally. Indirect draws may also bind an instance stream: the indirect buffer controls counts/firsts, while the instance buffer controls per-instance attributes.
4 unchanged lines hidden
62 64
63 65 | Backend | Compute barrier scope | Sampled target feedback | Runtime proof |
64 66 | --- | --- | --- | --- |
65 - | Vulkan | Conservative compute-write barrier for later shader reads/writes, vertex/index fetch, and indirect command reads. | Render pass final layouts come from target usage; descriptor layout is caller-declared. | Static checks now; device smoke gate still needed. |
67 + | Vulkan | Conservative compute-write barrier for later shader reads/writes, sampled texture reads, vertex/index/instance fetch, and indirect command reads. | Render pass final layouts come from target usage; descriptor layout is caller-declared. | Static checks now; device smoke gate still needed. |
66 68 | D3D11 | Immediate context resolves many SRV/RTV/UAV conflicts by auto-unbinding and debug warnings. Manual hazard cleanup needs slot tracking before it is safe. | No explicit image states; render-target/SRV overlap needs future warning-proof validation, not broad unbinds. | Windows runtime deferred. |
67 69 | D3D12 | Global UAV barrier for compute storage visibility; indirect draws transition argument buffers to `INDIRECT_ARGUMENT`. | Render/depth targets transition to write at pass begin and to shader-resource lazily when bound as sampled descriptors. | Windows runtime deferred. |
68 70 | OpenGL | Storage, texture-fetch, framebuffer, and command/indirect barriers are emitted after compute when requested. | Feedback-loop validation is a future diagnostics gap; current backend does not track attached texture handles at descriptor bind. | Linux device smoke gate still needed. |
159 unchanged lines hidden
228 230
229 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.
230 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.
231 - - `Multiple_Render_Targets` is implemented for the planned backend path. `Indirect_Draws` and `Stencil_Clips` remain false until those IR paths are fully implemented across all backends.
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.
232 234
233 235 Public API must not expose Vulkan/D3D-specific terms except through backend-only layers.
234 236
22 unchanged lines hidden
gpu/render_ir/access.odin +348 -0 added
1 + package render_ir
2 +
3 + import "core:mem"
4 +
5 + Resource_Access_Kind :: enum {
6 + Color_Attachment_Write,
7 + Depth_Attachment_Write,
8 + Sampled_Texture_Read,
9 + Storage_Buffer_Read_Write,
10 + Vertex_Buffer_Read,
11 + Index_Buffer_Read,
12 + Instance_Buffer_Read,
13 + Indirect_Argument_Read,
14 + Access_Metadata_Missing,
15 + }
16 +
17 + Resource_Access :: struct {
18 + command: Command_Handle,
19 + pass: Pass_Handle,
20 + resource: Resource_Handle,
21 + kind: Resource_Access_Kind,
22 + }
23 +
24 + Resource_Access_List :: [dynamic]Resource_Access
25 +
26 + destroy_resource_accesses :: proc(accesses: ^Resource_Access_List) {
27 + delete(accesses^)
28 + accesses^ = nil
29 + }
30 +
31 + collect_resource_accesses :: proc(
32 + frame: ^Frame_IR,
33 + allocator: mem.Allocator = context.allocator,
34 + ) -> Resource_Access_List {
35 + accesses := make(Resource_Access_List, 0, DEFAULT_COMMAND_CAP, allocator)
36 + if frame == nil {
37 + return accesses
38 + }
39 + plan := plan_frame_commands(frame, context.temp_allocator)
40 + defer command_plan_destroy(&plan)
41 + for command in plan {
42 + if command.culled {
43 + continue
44 + }
45 + switch command.record.kind {
46 + case .Draw:
47 + draw, draw_ok := get_draw(frame, command.handle)
48 + if !draw_ok {continue}
49 + collect_draw_accesses(frame, command.handle, draw, &accesses)
50 + case .Dispatch:
51 + dispatch, dispatch_ok := get_dispatch(frame, command.handle)
52 + if !dispatch_ok {continue}
53 + collect_dispatch_accesses(command.handle, dispatch, &accesses)
54 + }
55 + }
56 + return accesses
57 + }
58 +
59 + validate_resource_accesses :: proc(frame: ^Frame_IR, diagnostics: ^Diagnostic_List) -> bool {
60 + if frame == nil || diagnostics == nil {
61 + return false
62 + }
63 + ok := true
64 + plan := plan_frame_commands(frame, context.temp_allocator)
65 + defer command_plan_destroy(&plan)
66 + written_targets := make([dynamic]Resource_Handle, 0, DEFAULT_PASS_CAP, context.temp_allocator)
67 + known_frame_targets := collect_frame_target_resources(frame, context.temp_allocator)
68 + defer delete(known_frame_targets)
69 + seen_present := false
70 + for command in plan {
71 + if command.culled {
72 + continue
73 + }
74 + switch command.record.kind {
75 + case .Draw:
76 + draw, draw_ok := get_draw(frame, command.handle)
77 + if !draw_ok {continue}
78 + if draw_targets_present(frame, draw) {
79 + seen_present = true
80 + } else if seen_present && draw_targets_offscreen(frame, draw) {
81 + add_command_error(
82 + diagnostics,
83 + .Invalid_Order,
84 + command.handle,
85 + "offscreen work after present pass is unsupported",
86 + )
87 + ok = false
88 + }
89 + pass := effective_draw_pass(frame, draw)
90 + targets := pass_target_resources(frame, pass, context.temp_allocator)
91 + sampled := draw_sampled_resources(frame, draw, context.temp_allocator)
92 + for target in targets {
93 + for read in sampled {
94 + if target.resource != INVALID_RESOURCE && target.resource == read {
95 + add_command_error(
96 + diagnostics,
97 + .Resource_Feedback,
98 + command.handle,
99 + "draw samples a resource currently bound as a render target",
100 + )
101 + ok = false
102 + }
103 + }
104 + if target.resource != INVALID_RESOURCE {
105 + append_unique_resource(&written_targets, target.resource)
106 + }
107 + }
108 + for read in sampled {
109 + if resource_in(read, known_frame_targets[:]) && !resource_in(read, written_targets[:]) {
110 + add_command_error(
111 + diagnostics,
112 + .Invalid_Order,
113 + command.handle,
114 + "draw samples a frame target before it is written in planned order",
115 + )
116 + ok = false
117 + }
118 + }
119 + delete(targets)
120 + delete(sampled)
121 + case .Dispatch:
122 + }
123 + }
124 + return ok
125 + }
126 +
127 + @(private)
128 + collect_draw_accesses :: proc(
129 + frame: ^Frame_IR,
130 + command: Command_Handle,
131 + draw: ^Draw_Command,
132 + accesses: ^Resource_Access_List,
133 + ) {
134 + pass := effective_draw_pass(frame, draw)
135 + append_pass_target_accesses(frame, command, pass, accesses)
136 + geometry := draw.packet.geometry
137 + if geometry.resource != INVALID_RESOURCE {
138 + kind := Resource_Access_Kind.Index_Buffer_Read if geometry.kind == .Indexed_Mesh else Resource_Access_Kind.Vertex_Buffer_Read
139 + append(accesses, Resource_Access{command = command, pass = pass, resource = geometry.resource, kind = kind})
140 + }
141 + if draw.packet.instances.resource != INVALID_RESOURCE {
142 + append(
143 + accesses,
144 + Resource_Access {
145 + command = command,
146 + pass = pass,
147 + resource = draw.packet.instances.resource,
148 + kind = .Instance_Buffer_Read,
149 + },
150 + )
151 + }
152 + if draw.packet.instances.indirect != INVALID_RESOURCE {
153 + append(
154 + accesses,
155 + Resource_Access {
156 + command = command,
157 + pass = pass,
158 + resource = draw.packet.instances.indirect,
159 + kind = .Indirect_Argument_Read,
160 + },
161 + )
162 + }
163 + if material, material_ok := get_material(frame, draw.packet.material); material_ok {
164 + if material.texture_id != 0 || material.normal_map_texture_id != 0 {
165 + append(accesses, Resource_Access{command = command, pass = pass, kind = .Access_Metadata_Missing})
166 + }
167 + append_descriptor_sampled_accesses(frame, command, pass, material.descriptor_set, accesses)
168 + }
169 + }
170 +
171 + @(private)
172 + collect_dispatch_accesses :: proc(
173 + command: Command_Handle,
174 + dispatch: ^Dispatch_Command,
175 + accesses: ^Resource_Access_List,
176 + ) {
177 + for i in 0..<int(dispatch.storage_binding_count) {
178 + binding := dispatch.storage_bindings[i]
179 + if binding.resource == INVALID_RESOURCE {continue}
180 + append(
181 + accesses,
182 + Resource_Access {
183 + command = command,
184 + pass = dispatch.pass,
185 + resource = binding.resource,
186 + kind = .Storage_Buffer_Read_Write,
187 + },
188 + )
189 + }
190 + }
191 +
192 + @(private)
193 + append_pass_target_accesses :: proc(
194 + frame: ^Frame_IR,
195 + command: Command_Handle,
196 + pass: Pass_Handle,
197 + accesses: ^Resource_Access_List,
198 + ) {
199 + targets := pass_target_resources(frame, pass, context.temp_allocator)
200 + defer delete(targets)
201 + for target in targets {
202 + append(accesses, Resource_Access{command = command, pass = pass, resource = target.resource, kind = target.kind})
203 + }
204 + }
205 +
206 + @(private)
207 + append_descriptor_sampled_accesses :: proc(
208 + frame: ^Frame_IR,
209 + command: Command_Handle,
210 + pass: Pass_Handle,
211 + set_handle: Descriptor_Set_Handle,
212 + accesses: ^Resource_Access_List,
213 + ) {
214 + if set_handle == INVALID_DESCRIPTOR_SET {return}
215 + set, set_ok := get_descriptor_set(frame, set_handle)
216 + if !set_ok {return}
217 + for i in 0..<int(set.binding_count) {
218 + binding := set.bindings[i]
219 + if binding.type != .Combined_Image_Sampler || binding.resource == INVALID_RESOURCE {
220 + continue
221 + }
222 + append(
223 + accesses,
224 + Resource_Access {
225 + command = command,
226 + pass = pass,
227 + resource = binding.resource,
228 + kind = .Sampled_Texture_Read,
229 + },
230 + )
231 + }
232 + }
233 +
234 + @(private)
235 + Target_Access :: struct {
236 + resource: Resource_Handle,
237 + kind: Resource_Access_Kind,
238 + }
239 +
240 + @(private)
241 + pass_target_resources :: proc(
242 + frame: ^Frame_IR,
243 + pass_handle: Pass_Handle,
244 + allocator: mem.Allocator,
245 + ) -> [dynamic]Target_Access {
246 + targets := make([dynamic]Target_Access, 0, MAX_RENDER_TARGETS + 1, allocator)
247 + pass, pass_ok := get_pass(frame, pass_handle)
248 + if !pass_ok || pass.kind != .Render {
249 + return targets
250 + }
251 + for i in 0..<int(pass.color_target_count) {
252 + resource := pass.color_targets[i].resource
253 + if resource != INVALID_RESOURCE {
254 + append(&targets, Target_Access{resource = resource, kind = .Color_Attachment_Write})
255 + }
256 + }
257 + if pass.has_depth_target && pass.depth_target.resource != INVALID_RESOURCE {
258 + append(&targets, Target_Access{resource = pass.depth_target.resource, kind = .Depth_Attachment_Write})
259 + }
260 + return targets
261 + }
262 +
263 + @(private)
264 + draw_sampled_resources :: proc(
265 + frame: ^Frame_IR,
266 + draw: ^Draw_Command,
267 + allocator: mem.Allocator,
268 + ) -> [dynamic]Resource_Handle {
269 + reads := make([dynamic]Resource_Handle, 0, MAX_DESCRIPTOR_BINDINGS, allocator)
270 + if draw == nil {return reads}
271 + if material, material_ok := get_material(frame, draw.packet.material); material_ok {
272 + if set, set_ok := get_descriptor_set(frame, material.descriptor_set); set_ok {
273 + for i in 0..<int(set.binding_count) {
274 + binding := set.bindings[i]
275 + if binding.type == .Combined_Image_Sampler && binding.resource != INVALID_RESOURCE {
276 + append_unique_resource(&reads, binding.resource)
277 + }
278 + }
279 + }
280 + }
281 + return reads
282 + }
283 +
284 + @(private)
285 + collect_frame_target_resources :: proc(
286 + frame: ^Frame_IR,
287 + allocator: mem.Allocator,
288 + ) -> [dynamic]Resource_Handle {
289 + resources := make([dynamic]Resource_Handle, 0, len(frame.passes), allocator)
290 + for pass in frame.passes {
291 + if pass.kind != .Render {continue}
292 + for i in 0..<int(pass.color_target_count) {
293 + append_unique_resource(&resources, pass.color_targets[i].resource)
294 + }
295 + if pass.has_depth_target {
296 + append_unique_resource(&resources, pass.depth_target.resource)
297 + }
298 + }
299 + return resources
300 + }
301 +
302 + @(private)
303 + effective_draw_pass :: proc(frame: ^Frame_IR, draw: ^Draw_Command) -> Pass_Handle {
304 + if draw == nil {return INVALID_PASS}
305 + if draw.pass != INVALID_PASS {
306 + return draw.pass
307 + }
308 + if target, target_ok := get_target(frame, draw.packet.target); target_ok {
309 + return target.pass
310 + }
311 + return INVALID_PASS
312 + }
313 +
314 + @(private)
315 + draw_targets_present :: proc(frame: ^Frame_IR, draw: ^Draw_Command) -> bool {
316 + if draw == nil {return false}
317 + if target, target_ok := get_target(frame, draw.packet.target); target_ok {
318 + return target.kind == .Present
319 + }
320 + return draw.pass == INVALID_PASS
321 + }
322 +
323 + @(private)
324 + draw_targets_offscreen :: proc(frame: ^Frame_IR, draw: ^Draw_Command) -> bool {
325 + if draw == nil {return false}
326 + if target, target_ok := get_target(frame, draw.packet.target); target_ok {
327 + return target.kind == .Offscreen
328 + }
329 + return draw.pass != INVALID_PASS
330 + }
331 +
332 + @(private)
333 + append_unique_resource :: proc(resources: ^[dynamic]Resource_Handle, resource: Resource_Handle) {
334 + if resource == INVALID_RESOURCE || resource_in(resource, resources[:]) {
335 + return
336 + }
337 + append(resources, resource)
338 + }
339 +
340 + @(private)
341 + resource_in :: proc(resource: Resource_Handle, resources: []Resource_Handle) -> bool {
342 + for existing in resources {
343 + if existing == resource {
344 + return true
345 + }
346 + }
347 + return false
348 + }
gpu/render_ir/diagnostics.odin +4 -0 modified
27 unchanged lines hidden
28 28 Invalid_Order,
29 29 Unsupported,
30 30 Invalid_Descriptor,
31 + Resource_Feedback,
32 + Access_Metadata_Missing,
31 33 Internal_Error,
32 34 }
33 35
66 unchanged lines hidden
100 102 case .Invalid_Order: return "RIR0006"
101 103 case .Unsupported: return "RIR0007"
102 104 case .Invalid_Descriptor: return "RIR0008"
105 + case .Resource_Feedback: return "RIR0009"
106 + case .Access_Metadata_Missing: return "RIR0010"
103 107 case .Internal_Error: return "RIR9999"
104 108 }
105 109 return "RIR9999"
267 unchanged lines hidden
gpu/scripts/check.sh +6 -0 modified
25 unchanged lines hidden
26 26 echo "==> test: tests"
27 27 odin test tests -collection:window="$WINDOW_COLLECTION"
28 28
29 + echo "==> check: d3d12 transition boundary"
30 + if grep -n "Type = \\.TRANSITION\\|transition_resource(" backend/d3d12/d3d12_ops.odin backend/d3d12/d3d12_frame.odin; then
31 + echo "D3D12 transition barriers must go through typed helpers in d3d12_backend.odin" >&2
32 + exit 1
33 + fi
34 +
29 35 for backend in vulkan d3d11 d3d12 opengl; do
30 36 echo "==> check: gpu ($backend)"
31 37 odin check . -no-entry-point -collection:window="$WINDOW_COLLECTION" -define:GPU_BACKEND="$backend"
15 unchanged lines hidden
gpu/tests/render_ir/access_test.odin +177 -0 added
1 + package render_ir_tests
2 +
3 + import ir "../../render_ir"
4 + import "core:testing"
5 +
6 + has_access :: proc(
7 + accesses: []ir.Resource_Access,
8 + command: ir.Command_Handle,
9 + resource: ir.Resource_Handle,
10 + kind: ir.Resource_Access_Kind,
11 + ) -> bool {
12 + for access in accesses {
13 + if access.command == command && access.resource == resource && access.kind == kind {
14 + return true
15 + }
16 + }
17 + return false
18 + }
19 +
20 + has_metadata_gap :: proc(accesses: []ir.Resource_Access, command: ir.Command_Handle) -> bool {
21 + for access in accesses {
22 + if access.command == command && access.kind == .Access_Metadata_Missing {
23 + return true
24 + }
25 + }
26 + return false
27 + }
28 +
29 + make_sampled_material :: proc(
30 + frame: ^ir.Frame_IR,
31 + texture: ir.Resource_Handle,
32 + sampler: ir.Resource_Handle,
33 + ) -> ir.Material_Handle {
34 + layout := ir.make_descriptor_set_layout("set0")
35 + _ = ir.descriptor_layout_add_binding(&layout, 0, .Combined_Image_Sampler, 1, {.Fragment})
36 + layout_handle := ir.add_descriptor_set_layout(frame, layout)
37 + set := ir.make_descriptor_set("material", layout_handle)
38 + _ = ir.descriptor_set_bind_texture_sampler(&set, 0, texture, sampler)
39 + set_handle := ir.add_descriptor_set(frame, set)
40 + return ir.add_material(frame, {name = "mat", descriptor_set = set_handle})
41 + }
42 +
43 + @(test)
44 + test_resource_access_analyzer_classifies_known_resources :: proc(t: ^testing.T) {
45 + frame := ir.init_frame_ir()
46 + defer ir.destroy_frame_ir(&frame)
47 +
48 + color := ir.add_texture(&frame, "color", 16, 16, .R8G8B8A8_UNORM, {.Color_Attachment, .Sampled})
49 + sampled := ir.add_texture(&frame, "sampled", 16, 16, .R8G8B8A8_UNORM, {.Sampled})
50 + sampler := ir.add_sampler(&frame, "sampler", {})
51 + vertex := ir.add_buffer(&frame, "vertices", 256, {.Vertex}, {.Device_Local})
52 + instance := ir.add_buffer(&frame, "instances", 128, {.Vertex}, {.Device_Local})
53 + indirect := ir.add_buffer(&frame, "indirect", 64, {.Indirect_Argument}, {.Device_Local})
54 + storage := ir.add_buffer(&frame, "storage", 64, {.Storage}, {.Device_Local})
55 + pass_desc := ir.Pass_Desc{kind = .Render, name = "main"}
56 + _ = ir.pass_add_color_target(&pass_desc, ir.make_color_target(color, .R8G8B8A8_UNORM))
57 + pass := ir.add_pass(&frame, pass_desc)
58 + compute_pass := ir.add_pass(&frame, {kind = .Compute, name = "compute"})
59 + material := make_sampled_material(&frame, sampled, sampler)
60 + draw := ir.add_draw(
61 + &frame,
62 + {
63 + pass = pass,
64 + pipeline = ir.Pipeline_Handle(1),
65 + vertex_count = 6,
66 + packet = {
67 + material = material,
68 + geometry = {kind = .Vertex_Stream, resource = vertex, vertex_count = 6},
69 + instances = {resource = instance, indirect = indirect, stride = 16, count = 1},
70 + },
71 + },
72 + )
73 + dispatch_desc := ir.Dispatch_Command {
74 + pass = compute_pass,
75 + pipeline = ir.Pipeline_Handle(2),
76 + groups = {1, 1, 1},
77 + storage_binding_count = 1,
78 + }
79 + dispatch_desc.storage_bindings[0] = {binding = 0, type = .Storage_Buffer, resource = storage}
80 + _ = ir.add_dispatch(&frame, dispatch_desc)
81 +
82 + accesses := ir.collect_resource_accesses(&frame)
83 + defer ir.destroy_resource_accesses(&accesses)
84 + testing.expect(t, has_access(accesses[:], draw, color, .Color_Attachment_Write))
85 + testing.expect(t, has_access(accesses[:], draw, sampled, .Sampled_Texture_Read))
86 + testing.expect(t, has_access(accesses[:], draw, vertex, .Vertex_Buffer_Read))
87 + testing.expect(t, has_access(accesses[:], draw, instance, .Instance_Buffer_Read))
88 + testing.expect(t, has_access(accesses[:], draw, indirect, .Indirect_Argument_Read))
89 + testing.expect(t, has_access(accesses[:], ir.Command_Handle(2), storage, .Storage_Buffer_Read_Write))
90 + }
91 +
92 + @(test)
93 + test_resource_access_analyzer_marks_runtime_texture_metadata_gap :: proc(t: ^testing.T) {
94 + frame := ir.init_frame_ir()
95 + defer ir.destroy_frame_ir(&frame)
96 +
97 + pass := ir.add_pass(&frame, {kind = .Render, name = "main"})
98 + material := ir.add_material(&frame, {name = "runtime-texture", texture_id = 9})
99 + draw := ir.add_draw(
100 + &frame,
101 + {
102 + pass = pass,
103 + pipeline = ir.Pipeline_Handle(1),
104 + vertex_count = 3,
105 + packet = {material = material},
106 + },
107 + )
108 +
109 + accesses := ir.collect_resource_accesses(&frame)
110 + defer ir.destroy_resource_accesses(&accesses)
111 + testing.expect(t, has_metadata_gap(accesses[:], draw))
112 + }
113 +
114 + @(test)
115 + test_resource_access_validation_rejects_known_same_pass_feedback :: proc(t: ^testing.T) {
116 + frame := ir.init_frame_ir()
117 + defer ir.destroy_frame_ir(&frame)
118 +
119 + color := ir.add_texture(&frame, "color", 16, 16, .R8G8B8A8_UNORM, {.Color_Attachment, .Sampled})
120 + sampler := ir.add_sampler(&frame, "sampler", {})
121 + pass_desc := ir.Pass_Desc{kind = .Render, name = "main"}
122 + _ = ir.pass_add_color_target(&pass_desc, ir.make_color_target(color, .R8G8B8A8_UNORM))
123 + pass := ir.add_pass(&frame, pass_desc)
124 + material := make_sampled_material(&frame, color, sampler)
125 + draw := ir.add_draw(
126 + &frame,
127 + {
128 + pass = pass,
129 + pipeline = ir.Pipeline_Handle(1),
130 + vertex_count = 3,
131 + packet = {material = material},
132 + },
133 + )
134 + diagnostics := ir.init_diagnostics()
135 + defer ir.destroy_diagnostics(&diagnostics)
136 +
137 + testing.expect(t, !ir.validate_resource_accesses(&frame, &diagnostics))
138 + testing.expect_value(t, len(diagnostics.items), 1)
139 + testing.expect_value(t, diagnostics.items[0].command, draw)
140 + testing.expect_value(t, diagnostics.items[0].code, ir.Diagnostic_Code.Resource_Feedback)
141 + }
142 +
143 + @(test)
144 + test_resource_access_validation_rejects_offscreen_after_present :: proc(t: ^testing.T) {
145 + frame := ir.init_frame_ir()
146 + defer ir.destroy_frame_ir(&frame)
147 +
148 + present_pass := ir.add_pass(&frame, {kind = .Render, name = "present"})
149 + offscreen_pass := ir.add_pass(&frame, {kind = .Render, name = "offscreen"})
150 + present := ir.add_target(&frame, {kind = .Present, name = "present", width = 16, height = 16, pass = present_pass})
151 + offscreen := ir.add_target(&frame, {kind = .Offscreen, name = "offscreen", width = 16, height = 16, pass = offscreen_pass})
152 + _ = ir.add_draw(
153 + &frame,
154 + {
155 + pass = ir.INVALID_PASS,
156 + pipeline = ir.Pipeline_Handle(1),
157 + vertex_count = 3,
158 + packet = {target = present},
159 + },
160 + )
161 + offscreen_draw := ir.add_draw(
162 + &frame,
163 + {
164 + pass = offscreen_pass,
165 + pipeline = ir.Pipeline_Handle(1),
166 + vertex_count = 3,
167 + packet = {target = offscreen},
168 + },
169 + )
170 + diagnostics := ir.init_diagnostics()
171 + defer ir.destroy_diagnostics(&diagnostics)
172 +
173 + testing.expect(t, !ir.validate_resource_accesses(&frame, &diagnostics))
174 + testing.expect_value(t, len(diagnostics.items), 1)
175 + testing.expect_value(t, diagnostics.items[0].command, offscreen_draw)
176 + testing.expect_value(t, diagnostics.items[0].code, ir.Diagnostic_Code.Invalid_Order)
177 + }