package render_ir import "core:fmt" import "core:mem" import "core:strings" Source_Span :: struct { file: string, line_start: int, col_start: int, line_end: int, col_end: int, } Diagnostic_Severity :: enum { Error, Warning, Note, } Diagnostic_Code :: enum u32 { None, Invalid_Handle, Invalid_Resource, Invalid_Pass, Invalid_Pipeline, Invalid_Command, Invalid_Order, Unsupported, Invalid_Descriptor, Resource_Feedback, Access_Metadata_Missing, Invalid_Mask, Stencil_Unsupported, Internal_Error, } Diagnostic :: struct { severity: Diagnostic_Severity, code: Diagnostic_Code, message: string, // borrowed; caller owns dynamic message storage command: Command_Handle, span: Source_Span, } Diagnostic_List :: struct { items: [dynamic]Diagnostic, } init_diagnostics :: proc(allocator: mem.Allocator = context.allocator) -> Diagnostic_List { return Diagnostic_List{items = make([dynamic]Diagnostic, allocator)} } reset_diagnostics :: proc(list: ^Diagnostic_List) { clear(&list.items) } destroy_diagnostics :: proc(list: ^Diagnostic_List) { delete(list.items) list^ = {} } add_diagnostic :: proc( list: ^Diagnostic_List, severity: Diagnostic_Severity, code: Diagnostic_Code, message: string, command: Command_Handle = INVALID_COMMAND, span: Source_Span = {}, ) { append(&list.items, Diagnostic{ severity = severity, code = code, message = message, command = command, span = span, }) } add_error :: proc(list: ^Diagnostic_List, code: Diagnostic_Code, message: string, span: Source_Span = {}) { add_diagnostic(list, .Error, code, message, span = span) } add_warning :: proc(list: ^Diagnostic_List, code: Diagnostic_Code, message: string, span: Source_Span = {}) { add_diagnostic(list, .Warning, code, message, span = span) } add_note :: proc(list: ^Diagnostic_List, code: Diagnostic_Code, message: string, span: Source_Span = {}) { add_diagnostic(list, .Note, code, message, span = span) } add_command_error :: proc(list: ^Diagnostic_List, code: Diagnostic_Code, command: Command_Handle, message: string) { add_diagnostic(list, .Error, code, message, command = command) } format_code :: proc(code: Diagnostic_Code) -> string { switch code { case .None: return "RIR0000" case .Invalid_Handle: return "RIR0001" case .Invalid_Resource: return "RIR0002" case .Invalid_Pass: return "RIR0003" case .Invalid_Pipeline: return "RIR0004" case .Invalid_Command: return "RIR0005" case .Invalid_Order: return "RIR0006" case .Unsupported: return "RIR0007" case .Invalid_Descriptor: return "RIR0008" case .Resource_Feedback: return "RIR0009" case .Access_Metadata_Missing: return "RIR0010" case .Invalid_Mask: return "RIR0011" case .Stencil_Unsupported: return "RIR0012" case .Internal_Error: return "RIR9999" } return "RIR9999" } format_severity :: proc(severity: Diagnostic_Severity) -> string { switch severity { case .Error: return "error" case .Warning: return "warning" case .Note: return "note" } return "error" } has_span :: proc(span: Source_Span) -> bool { return span.line_start > 0 && span.col_start > 0 } format_span :: proc(span: Source_Span, allocator: mem.Allocator = context.allocator) -> string { if !has_span(span) { return "" } if span.file == "" { return fmt.aprintf("%v:%v", span.line_start, span.col_start, allocator = allocator) } return fmt.aprintf("%v:%v:%v", span.file, span.line_start, span.col_start, allocator = allocator) } format_diagnostic :: proc(d: Diagnostic, allocator: mem.Allocator = context.allocator) -> string { severity := format_severity(d.severity) message := d.message if d.command != INVALID_COMMAND { message = fmt.aprintf("command #%d: %s", d.command, d.message, allocator = context.temp_allocator) } if d.code == .None { if has_span(d.span) { loc := format_span(d.span, allocator = context.temp_allocator) return fmt.aprintf("%v: %v: %v", loc, severity, message, allocator = allocator) } return fmt.aprintf("%v: %v", severity, message, allocator = allocator) } code := format_code(d.code) if has_span(d.span) { loc := format_span(d.span, allocator = context.temp_allocator) return fmt.aprintf("%v: %v[%v]: %v", loc, severity, code, message, allocator = allocator) } return fmt.aprintf("%v[%v]: %v", severity, code, message, allocator = allocator) } format_diagnostic_with_source :: proc(d: Diagnostic, source: string, allocator: mem.Allocator = context.allocator) -> string { if source == "" || !has_span(d.span) { return format_diagnostic(d, allocator) } line_num := 1 line_start_idx := 0 for i := 0; i < len(source); i += 1 { if line_num == d.span.line_start { line_start_idx = i break } if source[i] == '\n' { line_num += 1 } } if line_num != d.span.line_start { return format_diagnostic(d, allocator) } line_end_idx := line_start_idx for line_end_idx < len(source) && source[line_end_idx] != '\n' { line_end_idx += 1 } line_text := source[line_start_idx:line_end_idx] col_start := max(d.span.col_start - 1, 0) col_end := max(d.span.col_end, d.span.col_start) underline_len := max(col_end - col_start, 1) if col_start + underline_len > len(line_text) { underline_len = max(len(line_text) - col_start, 1) } base := format_diagnostic(d, allocator = context.temp_allocator) buf := strings.builder_make(allocator) strings.write_string(&buf, base) strings.write_string(&buf, "\n") line_number := fmt.aprintf("%d", d.span.line_start, allocator = context.temp_allocator) for _ in 0..<(4 - min(len(line_number), 4)) { strings.write_byte(&buf, ' ') } fmt.sbprintf(&buf, "%s | %s\n", line_number, line_text) strings.write_string(&buf, " | ") for _ in 0.. bool { for d in list.items { if d.severity == .Error { return true } } return false } has_errors_slice :: proc(diagnostics: []Diagnostic) -> bool { for d in diagnostics { if d.severity == .Error { return true } } return false } format_is_depth :: proc(format: Format) -> bool { switch format { case .Undefined: return false case .R8G8B8A8_SRGB, .R8G8B8A8_UNORM, .B8G8R8A8_SRGB: return false case .D32_SFLOAT, .D32_SFLOAT_S8_UINT, .D24_UNORM_S8_UINT: return true } return false } format_is_color :: proc(format: Format) -> bool { return format != .Undefined && !format_is_depth(format) } validate_render_target :: proc(ir: ^Frame_IR, pass_index: int, target: Render_Target_Desc, diagnostics: ^Diagnostic_List) { resource, resource_ok := get_resource(ir, target.resource) if !resource_ok { add_error(diagnostics, .Invalid_Resource, "pass target uses invalid resource handle") return } if resource.kind != .Texture { add_error(diagnostics, .Invalid_Resource, "pass target resource is not a texture") return } if target.kind == .Color { if .Color_Attachment not_in resource.texture.usage { add_error(diagnostics, .Invalid_Resource, "color target resource lacks Color_Attachment usage") } if !format_is_color(target.format) { add_error(diagnostics, .Invalid_Pass, "color target has non-color format") } } else { if .Depth_Stencil_Attachment not_in resource.texture.usage { add_error(diagnostics, .Invalid_Resource, "depth target resource lacks Depth_Stencil_Attachment usage") } if !format_is_depth(target.format) { add_error(diagnostics, .Invalid_Pass, "depth target has non-depth format") } } } validate_pass_targets :: proc(ir: ^Frame_IR, diagnostics: ^Diagnostic_List) { for pass, pass_index in ir.passes { if pass.kind != .Render { if pass.color_target_count > 0 || pass.has_depth_target { add_error(diagnostics, .Invalid_Pass, "non-render pass has render targets") } continue } for target_index in 0..= MAX_COMPUTE_BINDINGS { add_error(diagnostics, .Invalid_Resource, "dispatch storage binding index is out of range") continue } binding_index := int(binding.binding) if binding_index in seen_bindings { add_error(diagnostics, .Invalid_Resource, "dispatch storage binding index is duplicated") continue } seen_bindings += {binding_index} buffer, buffer_ok := get_resource(ir, binding.resource) if !buffer_ok || buffer.kind != .Buffer { add_error(diagnostics, .Invalid_Resource, "dispatch storage binding requires buffer resource") } else if !(.Storage in buffer.buffer.usage) { add_error(diagnostics, .Invalid_Resource, "dispatch storage binding buffer missing Storage usage") } } } } validate_mask_clips_supported :: proc(ir: ^Frame_IR, diagnostics: ^Diagnostic_List, stencil_clips_supported: bool) { if stencil_clips_supported { return } for clip in ir.clips { if clip.kind == .Mask { add_error(diagnostics, .Stencil_Unsupported, "mask clips require backend Stencil_Clips support") } } }