Debugging and Observability

Goldy provides validation layers, structured instrumentation, and environment variable controls that together cover the full debugging workflow — from catching API misuse to profiling frame timing.

Validation

GOLDY_VALIDATION Environment Variable

The primary control for runtime validation. Accepts a comma-, semicolon-, or whitespace-separated list of categories:

ValueEffect
apiEnable backend GPU API validation (see below)
layoutEnable Rust ↔ Slang struct layout checks and buffer stride checks
allEnable both api and layout
1, true, yesGPU API validation only (legacy shorthand; does not enable layout checks)

Categories can be combined:

# API validation only
GOLDY_VALIDATION=api cargo run --example triangle

# Layout validation only
GOLDY_VALIDATION=layout cargo run --example triangle

# Both
GOLDY_VALIDATION=all cargo run --example triangle
GOLDY_VALIDATION=layout,api cargo run --example triangle

API Validation

When GOLDY_VALIDATION includes api (or 1/true/yes), Goldy enables backend-specific validation:

BackendWhat Gets Enabled
VulkanVK_LAYER_KHRONOS_validation + VK_EXT_debug_utils at instance creation
MetalSets MTL_SHADER_VALIDATION=1 (if not already set) before the first device is created
DX12See DX12 Debug Layer below

For Vulkan, validation is also enabled when VK_INSTANCE_LAYERS contains VK_LAYER_KHRONOS_validation (the standard loader-driven workflow).

Layout Validation

Layout validation catches mismatches between Rust struct layouts and their Slang shader counterparts at shader compile time, and buffer element-stride mismatches at dispatch time.

Enable via either:

GOLDY_VALIDATION=layout  cargo run
GOLDY_VALIDATE_LAYOUTS=1 cargo run   # legacy variable, equivalent

#[derive(LayoutCheckable)]

Annotate Rust structs that mirror Slang types to opt into automatic validation:

#![allow(unused)]
fn main() {
#[derive(LayoutCheckable)]
#[repr(C)]
struct SceneUniforms {
    projection: [[f32; 4]; 4],
    view: [[f32; 4]; 4],
    time: f32,
}
}

The derive macro generates a LAYOUT_CHECK constant containing the struct's name, total size, and per-field offsets. Pass it when creating a shader module:

#![allow(unused)]
fn main() {
let shader = ShaderModule::from_slang_with_options(
    &device,
    source,
    &[],          // extra search paths
    &[],          // defines
    Default::default(),
    &[SceneUniforms::LAYOUT_CHECK],
)?;
}

When layout validation is enabled, Goldy compiles the Slang shader, reflects each named struct, and compares:

  • Total struct size — Rust size_of vs. Slang reflection
  • Field offsets — each named field's byte offset

A mismatch produces an error naming the struct, the field, and the expected vs. actual offset — immediately surfacing padding or alignment bugs. When validation is disabled, the checks are skipped at zero cost.

Buffer Stride Validation

At dispatch time (when layout validation is enabled), Goldy also checks that each bound buffer's element_stride matches the stride the shader expects from Slang reflection. A mismatch produces an error like:

buffer element-stride mismatch in shader `my_shader`:
  slot 0: shader expects element stride 16 but buffer has 4

DX12-Specific Debugging

DX12 Debug Layer

VariableValuesEffect
GOLDY_DX12_DEBUG1Force-enable the D3D12 debug layer (even in release builds)
GOLDY_DX12_NO_DEBUG1Disable the D3D12 debug layer (useful for parallel tests that crash the debug layer)
GOLDY_DX12_GBV1Enable GPU-Based Validation (very slow; requires the debug layer)

GPU-Based Validation (GBV) instruments shaders on the GPU to detect issues that the CPU-side debug layer cannot catch — such as out-of-bounds descriptor accesses and uninitialized resource reads. Expect a significant performance hit.

WARP Software Rasterizer

WARP is Microsoft's software implementation of D3D12. It runs on the CPU, so it works on headless CI runners with no GPU.

GOLDY_DX12_FORCE_WARP=1 cargo nextest run

After the first WARP device is created, Goldy prints a confirmation:

[WARP] d3d10warp.dll loaded from: C:\WINDOWS\SYSTEM32\d3d10warp.dll

On Windows, DX12 is the default backend, so GOLDY_DX12_FORCE_WARP=1 is the only variable you need to run tests on a machine without a GPU.

Structured Instrumentation

Goldy includes a structured instrumentation system built on the tracing crate. It provides named observation points with hierarchical dot-notation names and structured context data.

Enabling Instrumentation

Instrumentation requires the instrumentation Cargo feature (enabled by default). When disabled, all macros compile to no-ops at zero cost.

# Explicitly enable
cargo build --features instrumentation

# Disable (zero-cost removal)
cargo build --no-default-features --features vulkan

goldy_span! — Timed Sections

Create a span to measure the duration of a code section:

#![allow(unused)]
fn main() {
use goldy::goldy_span;

fn compile_shader(&self) {
    let _span = goldy_span!("slang.compile", target = "metal").entered();
    // ... compilation code ...
    // Duration is recorded automatically when _span is dropped
}
}

goldy_event! — Instant Markers

Emit a one-shot structured event:

#![allow(unused)]
fn main() {
use goldy::goldy_event;

goldy_event!("slang.library.load",
    path = %lib_path.display(),
    success = true
);
}

Built-in Observation Points

Goldy instruments its own internals at these observation points:

CategoryPoint NameEmitted Data
Slangslang.library.loadpath, success
slang.compile.starttarget, entry_points, bindless
slang.compile.endduration_ms, output_size, success
slang.reflection.extractparameter_blocks, fields
Shadershader.module.createbackend, shader_type
shader.pipeline.createpipeline_type, bind_groups
Resourceresource.buffer.createsize, usage
resource.texture.createdimensions, format
resource.bind_group.createbindings_count
Renderrender.frame.startframe_id
render.compute.dispatchworkgroups, pipeline
render.drawvertices, instances
render.frame.endframe_id, duration_ms

JSON Logging

Install a JSON file logger to capture all instrumentation output as structured JSON:

#![allow(unused)]
fn main() {
use goldy::instrumentation::install_json_logger;

install_json_logger("/tmp/goldy-debug.json")?;

// All subsequent goldy_span!/goldy_event! calls are written to the file
}

Filtering with RUST_LOG

Use the standard RUST_LOG environment variable to control verbosity. All Goldy instrumentation uses the goldy target:

RUST_LOG=goldy=debug cargo run --example triangle
RUST_LOG=goldy::render=trace cargo run --example triangle

Environment Variables Summary

VariableValuesEffect
GOLDY_BACKENDvulkan/vk, dx12/d3d12/directx, metal/mtlOverride backend selection
GOLDY_VALIDATIONapi, layout, all, 1/true/yesEnable validation categories
GOLDY_VALIDATE_LAYOUTS1, true, yesEnable layout validation (legacy; prefer GOLDY_VALIDATION=layout)
GOLDY_DX12_FORCE_WARP1Use WARP software rasterizer
GOLDY_DX12_DEBUG1Force-enable D3D12 debug layer in release
GOLDY_DX12_NO_DEBUG1Disable D3D12 debug layer
GOLDY_DX12_GBV1Enable GPU-Based Validation
RUST_LOGe.g. goldy=debugFilter instrumentation output

Common Debugging Patterns

Catch API misuse early

GOLDY_VALIDATION=api cargo run --example my_app

Turn on API validation during development to catch invalid GPU API calls. On Vulkan this enables the Khronos validation layer; on Metal it enables shader validation.

Diagnose struct layout bugs

GOLDY_VALIDATION=layout cargo test

If a LayoutCheckable struct diverges from its Slang counterpart (due to padding, alignment, or a field being added on only one side), the error message names the exact struct and field.

Headless CI on Windows

GOLDY_DX12_FORCE_WARP=1 cargo nextest run

WARP gives you a fully functional D3D12 device on machines with no GPU. Combine with GOLDY_VALIDATION=api for maximum coverage.

Profile frame timing

#![allow(unused)]
fn main() {
use goldy::instrumentation::install_json_logger;

install_json_logger("/tmp/goldy-profile.json")?;

// Run your application, then inspect the JSON output for
// render.frame.start / render.frame.end durations
}

Deep DX12 debugging

GOLDY_DX12_DEBUG=1 GOLDY_DX12_GBV=1 cargo run --example my_app

GPU-Based Validation catches GPU-side issues the CPU debug layer cannot see, at a significant performance cost. Use it when you suspect descriptor or resource access bugs.