Definition:
Frame Buffer Hashing is a visual regression testing technique that verifies rendered graphics by reading the raw GPU framebuffer (the pixel data before any compression or encoding), computing a cryptographic hash of those bytes, and comparing it against a known-good reference hash. If the hashes match, every pixel is identical. If they don’t, something changed.
In one sentence: “Take a fingerprint of the raw pixels straight from the GPU, then compare it to the golden fingerprint — if they match, the render is perfect.”
Analogy — The Painting Authenticator Imagine an art authenticator verifying that a famous painting hasn’t been tampered with. She has two approaches:
Approach What happens Result 📸 Take a photo, then compare photos Photo compression introduces artifacts. Two identical paintings shot with different camera settings produce different JPEGs. “They don’t match!” → false alarm 😤 🔬 Scan the canvas directly She places a high-res scanner directly on the original canvas, captures raw pigment data, and hashes it. Same canvas = same hash. Always. “Hash matches. It’s authentic.” ✅ The photo is like a PNG screenshot — encoded, compressed, lossy. The direct scan is like the raw framebuffer — pure, unmodified pixel data from the GPU.
Example:
Frame Buffer Hashing in Practice
Here's a simplified version based on real GPU testing frameworks (Intel GPU Tools, Chromium's video frame validator):
import hashlib
import struct
from dataclasses import dataclass
from typing import Dict
@dataclass
class FrameBuffer:
"""Represents a raw GPU framebuffer mapped to CPU memory."""
width: int
height: int
bytes_per_pixel: int # e.g., 4 for RGBA
raw_bytes: bytes # raw pixel data straight from the GPU
def hash_framebuffer(fb: FrameBuffer) -> str:
"""
Compute an MD5 hash of the raw framebuffer bytes.
No encoding, no compression — pure pixel data.
"""
# In real GPU testing, you'd map the framebuffer from GPU memory:
# ptr = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)
# fb.raw_bytes = ptr[0 : width * height * bpp]
return hashlib.md5(fb.raw_bytes).hexdigest()
# ── Golden Reference Store ──
# Stored once when the test is first created.
# A few KB for hundreds of frames (vs. GBs of PNGs).
GOLDEN_HASHES: Dict[str, str] = {
"main_menu": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"settings_screen": "1f2e3d4c5b6a79808182838485868788",
"gameplay_frame_1": "deadbeefcafebabedeadbeefcafebabe",
}
def visual_regression_test(scene_name: str, fb: FrameBuffer) -> bool:
"""
Run a single visual regression test.
Returns True if the frame matches the golden reference.
"""
actual_hash = hash_framebuffer(fb)
expected_hash = GOLDEN_HASHES.get(scene_name)
if expected_hash is None:
# New scene — store this as the golden reference
print(f"✨ No golden hash for '{scene_name}'. Storing: {actual_hash}")
GOLDEN_HASHES[scene_name] = actual_hash
return True
if actual_hash == expected_hash:
print(f"✅ {scene_name}: PASS (hash matches)")
return True
else:
print(f"❌ {scene_name}: FAIL")
print(f" Expected: {expected_hash}")
print(f" Got: {actual_hash}")
# Save the actual frame as a PNG for human review
save_frame_as_png(fb, f"failure_artifacts/{scene_name}.png")
return False
def save_frame_as_png(fb: FrameBuffer, path: str):
"""Only save a PNG when the test FAILS — for manual inspection."""
# Convert raw bytes to PNG (using PIL, stb_image, etc.)
# This is the ONLY time encoding happens — after failure.
print(f" 📸 Saved failure artifact: {path}")
# ── Usage ──
# Simulate a correct render
correct_frame = FrameBuffer(
width=1920, height=1080, bytes_per_pixel=4,
raw_bytes=b'\xff\x00\x00\xff' * (1920 * 1080) # all red pixels
)
test1 = visual_regression_test("main_menu", correct_frame)
# Simulate a rendering bug — one pixel is wrong
buggy_bytes = bytearray(b'\xff\x00\x00\xff' * (1920 * 1080))
buggy_bytes[0] = 0xfe # slightly different red in first pixel
buggy_frame = FrameBuffer(
width=1920, height=1080, bytes_per_pixel=4,
raw_bytes=bytes(buggy_bytes)
)
test2 = visual_regression_test("main_menu", buggy_frame)
Output:
✅ main_menu: PASS (hash matches)
❌ main_menu: FAIL
Expected: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
Got: 7f8e9d0c1b2a39485748392a1b0c0d0e
📸 Saved failure artifact: failure_artifacts/main_menu.png