///|
/// `GraphicsDriver` trait and framebuffer-snapshot utilities used for VRT.
///
/// `GraphicsDriver` is the backend-facing API a renderer talks to;
/// `FramebufferSnapshot` + `compare_framebuffer_snapshots` are a small
/// pixel-diff harness for visual regression checks against `read_pixels`.
///|
/// The contract a graphics backend implements to render a frame.
///
/// One driver instance owns the device, the swapchain / surface, and
/// the per-frame command lifecycle. Renderers talk to a driver through
/// this trait and never reach for backend-specific APIs.
///
/// A typical frame:
///
/// ```ignore
/// driver.begin(pass)
/// for command in commands {
/// driver.draw_triangles(command)
/// }
/// driver.end(present)
/// ```
///
/// More commonly, drive it from a `CommandQueue` via `flush_commands`.
///
/// Implementors should be idempotent on `initialize` and should
/// de-duplicate identical `resize` calls.
pub(open) trait GraphicsDriver {
/// Bind a device / swapchain. Called once before any other method.
/// Ebiten ref: Graphics.Initialize.
fn initialize(Self) -> Unit raise
/// Open a render pass with the given clear color and present flag.
/// Ebiten ref: Graphics.Begin.
fn begin(Self, pass : RenderPassDesc) -> Unit raise
/// Close the current render pass. `present=true` swaps buffers.
/// Ebiten ref: Graphics.End.
fn end(Self, present : Bool) -> Unit raise
/// Reconfigure the surface to the new size. May be called outside a
/// render pass and should de-duplicate identical sizes.
/// Ebiten ref: surface resize / reconfigure paths.
fn resize(Self, width : Int, height : Int) -> Unit raise
/// Allocate a backend-owned image of the requested size and return a
/// handle. The numeric id inside the handle is backend-defined; the
/// caller treats it as opaque.
/// Ebiten ref: Graphics.NewImage.
fn new_image(Self, width : Int, height : Int) -> ImageHandle raise
/// Compile a shader source string and return a handle.
/// Ebiten ref: Graphics.NewShader.
fn new_shader(Self, source : String) -> ShaderHandle raise
/// Submit one `DrawTrianglesCommand` into the current render pass.
/// Must be called between `begin` and `end`.
/// Ebiten ref: Graphics.DrawTriangles.
fn draw_triangles(Self, command : DrawTrianglesCommand) -> Unit raise
/// Read back framebuffer pixels as a flat RGBA8 array of length
/// `width * height * 4`. Returns None if readback is not supported by
/// the backend (e.g. some headless paths). Used for VRT / golden
/// image testing.
fn read_pixels(Self, x : Int, y : Int, width : Int, height : Int) -> Array[
Int,
]? raise
}
///|
pub struct FramebufferSnapshot {
x : Int
y : Int
width : Int
height : Int
pixels : Array[Int]
} derive(Debug)
///|
pub impl Show for FramebufferSnapshot with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
/// Wrap an already-captured RGBA8 pixel buffer into a snapshot. Useful
/// when the pixels come from a non-`GraphicsDriver` source (golden
/// fixtures on disk, a manual PNG decode, ...). The buffer must be
/// `width * height * 4` bytes; this is not checked here.
pub fn FramebufferSnapshot::from_pixels(
x : Int,
y : Int,
width : Int,
height : Int,
pixels : Array[Int],
) -> FramebufferSnapshot {
{ x, y, width, height, pixels }
}
///|
pub fn create_framebuffer_snapshot(
driver : &GraphicsDriver,
x : Int,
y : Int,
width : Int,
height : Int,
) -> FramebufferSnapshot? raise {
match driver.read_pixels(x, y, width, height) {
Some(pixels) =>
Some(FramebufferSnapshot::from_pixels(x, y, width, height, pixels))
None => None
}
}
///|
pub struct PixelDiffResult {
total_pixels : Int
diff_pixels : Int
max_channel_diff : Int
} derive(Debug)
///|
pub impl Show for PixelDiffResult with fn output(self, logger) {
logger.write_object(to_repr(self))
}
///|
/// Per-channel diff against another snapshot. Any channel whose
/// absolute difference exceeds `threshold` counts the pixel as
/// changed; mismatched dimensions report all pixels as different.
pub fn FramebufferSnapshot::compare_with(
self : FramebufferSnapshot,
other : FramebufferSnapshot,
threshold : Int,
) -> PixelDiffResult {
let total = self.width * self.height
if self.width != other.width || self.height != other.height {
return { total_pixels: total, diff_pixels: total, max_channel_diff: 255 }
}
let pixel_count = self.width * self.height
let mut diff_pixels = 0
let mut max_diff = 0
for i = 0; i < pixel_count; i = i + 1 {
let base = i * 4
if base + 3 < self.pixels.length() && base + 3 < other.pixels.length() {
let dr = abs_int(self.pixels[base] - other.pixels[base])
let dg = abs_int(self.pixels[base + 1] - other.pixels[base + 1])
let db = abs_int(self.pixels[base + 2] - other.pixels[base + 2])
let da = abs_int(self.pixels[base + 3] - other.pixels[base + 3])
let channel_max = @cmp.maximum(@cmp.maximum(dr, dg), @cmp.maximum(db, da))
if channel_max > max_diff {
max_diff = channel_max
}
if channel_max > threshold {
diff_pixels = diff_pixels + 1
}
}
}
{ total_pixels: pixel_count, diff_pixels, max_channel_diff: max_diff }
}
///|
fn abs_int(x : Int) -> Int {
if x < 0 {
-x
} else {
x
}
}
///|
/// Fraction of pixels that exceeded the diff threshold, in [0.0, 1.0].
pub fn PixelDiffResult::diff_ratio(self : PixelDiffResult) -> Double {
if self.total_pixels == 0 {
return 0.0
}
self.diff_pixels.to_double() / self.total_pixels.to_double()
}