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