Harbor

branch main
showing the latest snapshot on main
diagnostics.odin 13.1 KB · Plain text
render_ir/diagnostics.odin 0644 Raw
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..<col_start {
		strings.write_byte(&buf, ' ')
	}
	for _ in 0..<underline_len {
		strings.write_byte(&buf, '^')
	}
	return strings.to_string(buf)
}

has_errors :: proc(list: ^Diagnostic_List) -> 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..<int(pass.color_target_count) {
			validate_render_target(ir, pass_index, pass.color_targets[target_index], diagnostics)
		}
		if pass.has_depth_target {
			validate_render_target(ir, pass_index, pass.depth_target, diagnostics)
		}
	}
}

validate_intent_handles :: proc(ir: ^Frame_IR, diagnostics: ^Diagnostic_List) {
	for layer in ir.layers {
		if _, ok := get_target(ir, layer.target); !ok {
			add_error(diagnostics, .Invalid_Handle, "layer uses invalid target handle")
		}
	}

	for clip in ir.clips {
		if clip.target != INVALID_TARGET {
			if _, ok := get_target(ir, clip.target); !ok {
				add_error(diagnostics, .Invalid_Handle, "clip uses invalid target handle")
			}
		}
		if clip.kind == .Mask {
			mask, mask_ok := get_mask(ir, clip.mask)
			if !mask_ok {
				add_error(diagnostics, .Invalid_Mask, "mask clip uses invalid mask handle")
				continue
			}
			if mask.target != clip.target {
				add_error(diagnostics, .Invalid_Mask, "mask clip target does not match mask target")
			}
		}
	}

	for mask in ir.masks {
		if _, ok := get_target(ir, mask.target); !ok {
			add_error(diagnostics, .Invalid_Mask, "mask uses invalid target handle")
		}
		switch mask.source_kind {
		case .None:
			add_error(diagnostics, .Invalid_Mask, "mask requires a source")
		case .Geometry_Coverage:
			resource, resource_ok := get_resource(ir, mask.coverage_resource)
			if !resource_ok || resource.kind != .Buffer {
				add_error(diagnostics, .Invalid_Mask, "mask geometry coverage requires a buffer resource")
			} else if .Vertex not_in resource.buffer.usage {
				add_error(diagnostics, .Invalid_Mask, "mask geometry coverage buffer lacks Vertex usage")
			}
			if mask.vertex_count == 0 && mask.index_count == 0 {
				add_error(diagnostics, .Invalid_Mask, "mask geometry coverage has no vertices or indices")
			}
			if mask.index_resource != INVALID_RESOURCE {
				index_resource, index_ok := get_resource(ir, mask.index_resource)
				if !index_ok || index_resource.kind != .Buffer {
					add_error(diagnostics, .Invalid_Mask, "mask geometry index source requires a buffer resource")
				} else if .Index not_in index_resource.buffer.usage {
					add_error(diagnostics, .Invalid_Mask, "mask geometry index buffer lacks Index usage")
				}
			}
		}
	}

	for draw in ir.draw_commands {
		if !draw_kind_is_packet(draw.kind) {
			continue
		}

		packet := draw.packet
		if _, ok := get_target(ir, packet.target); !ok {
			add_error(diagnostics, .Invalid_Handle, "draw packet uses invalid target handle")
		}
		if _, ok := get_layer(ir, packet.layer); !ok {
			add_error(diagnostics, .Invalid_Handle, "draw packet uses invalid layer handle")
		}
		if packet.clip != INVALID_CLIP {
			if _, ok := get_clip(ir, packet.clip); !ok {
				add_error(diagnostics, .Invalid_Handle, "draw packet uses invalid clip handle")
			}
		}

		if packet.material != INVALID_MATERIAL {
			if _, ok := get_material(ir, packet.material); !ok {
				add_error(diagnostics, .Invalid_Handle, "draw packet uses invalid material handle")
			}
		}

		if packet.geometry.resource != INVALID_RESOURCE {
			if _, ok := get_resource(ir, packet.geometry.resource); !ok {
				add_error(diagnostics, .Invalid_Resource, "draw packet geometry uses invalid resource handle")
			}
		}
		if packet.instances.resource != INVALID_RESOURCE {
			if _, ok := get_resource(ir, packet.instances.resource); !ok {
				add_error(diagnostics, .Invalid_Resource, "draw packet instances use invalid resource handle")
			}
		}
		if packet.instances.indirect != INVALID_RESOURCE {
			if _, ok := get_resource(ir, packet.instances.indirect); !ok {
				add_error(diagnostics, .Invalid_Resource, "draw packet indirect args use invalid resource handle")
			}
		}
	}

	for dispatch in ir.dispatch_commands {
		if dispatch.shader_id == 0 {
			add_error(diagnostics, .Invalid_Handle, "dispatch uses invalid compute shader id")
		}
		if dispatch.groups[0] == 0 || dispatch.groups[1] == 0 || dispatch.groups[2] == 0 {
			add_error(diagnostics, .Invalid_Command, "dispatch uses zero workgroup dimension")
		}
		seen_bindings: bit_set[0..<MAX_COMPUTE_BINDINGS]
		count := min(int(dispatch.storage_binding_count), MAX_COMPUTE_BINDINGS)
		for i in 0..<count {
			binding := dispatch.storage_bindings[i]
			if binding.type != .Storage_Buffer {
				add_error(diagnostics, .Invalid_Resource, "dispatch storage binding has non-storage descriptor type")
				continue
			}
			if binding.binding >= 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")
		}
	}
}