#![cfg(test)]
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
use wgpu::TextureFormat;
#[test]
fn returns_lock_when_not_poisoned() {
let mutex = Mutex::new(HashMap::<u64, u32>::new());
let guard = GpuRenderer::lock_or_clear_cache(&mutex);
assert!(guard.is_empty());
}
#[test]
fn clears_cache_when_poisoned() {
let mutex = Mutex::new(HashMap::<u64, u32>::new());
{
let mut guard = mutex.lock().unwrap();
guard.insert(1, 100);
guard.insert(2, 200);
}
let result = std::panic::catch_unwind(|| {
let mutex = std::panic::AssertUnwindSafe(&mutex);
let _guard = mutex.lock().unwrap();
panic!("intentional panic to poison the mutex");
});
assert!(result.is_err(), "the inner panic should propagate");
let guard = GpuRenderer::lock_or_clear_cache(&mutex);
assert!(
guard.is_empty(),
"cache must be cleared after poison recovery, got {:?}",
*guard
);
}
#[test]
fn works_with_vec_cache() {
let mutex = Mutex::new(Vec::<u32>::new());
{
let mut guard = mutex.lock().unwrap();
guard.push(1);
guard.push(2);
guard.push(3);
}
let _ = std::panic::catch_unwind(|| {
let mutex = std::panic::AssertUnwindSafe(&mutex);
let _guard = mutex.lock().unwrap();
panic!("poison");
});
let guard = GpuRenderer::lock_or_clear_cache(&mutex);
assert!(guard.is_empty(), "Vec cache should be cleared on poison");
}
#[test]
fn test_wgsl() {
let source = include_str!("../shaders/effects.wgsl");
let mut frontend = naga::front::wgsl::Frontend::new();
match frontend.parse(source) {
Ok(_) => println!("WGSL parsed successfully!"),
Err(e) => {
panic!("WGSL parsing failed: \n{}", e.emit_to_string(source));
}
}
}
#[test]
fn test_wgsl_common_uses_binding_array_on_native() {
let source = include_str!("../shaders/common.wgsl");
assert!(
source.contains("binding_array<texture_2d<f32>, 32>"),
"native common.wgsl must declare a 32-element texture binding_array"
);
assert!(
source.contains("t_diffuse:"),
"native common.wgsl must declare t_diffuse"
);
}
#[test]
fn test_wgsl_native_indexed_access() {
let bloom = include_str!("../shaders/bloom.wgsl");
let material = include_str!("../shaders/material_opaque.wgsl");
assert!(
bloom.contains("t_diffuse["),
"native bloom.wgsl must index t_diffuse as an array"
);
assert!(
material.contains("t_diffuse["),
"native material_opaque.wgsl must index t_diffuse as an array"
);
}
#[derive(Clone)]
struct Sha256 {
state: [u32; 8],
buffer: [u8; 64],
buffer_len: usize,
total_len: u64,
}
impl Sha256 {
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
fn new() -> Self {
Self {
state: [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
],
buffer: [0; 64],
buffer_len: 0,
total_len: 0,
}
}
fn update(&mut self, data: &[u8]) {
self.total_len = self.total_len.wrapping_add(data.len() as u64);
for &b in data {
self.buffer[self.buffer_len] = b;
self.buffer_len += 1;
if self.buffer_len == 64 {
let block = self.buffer;
self.compress(&block);
self.buffer_len = 0;
}
}
}
fn finalize(mut self) -> [u8; 32] {
self.buffer[self.buffer_len] = 0x80;
self.buffer_len += 1;
if self.buffer_len > 56 {
for b in &mut self.buffer[self.buffer_len..] {
*b = 0;
}
let block = self.buffer;
self.compress(&block);
self.buffer_len = 0;
}
for b in &mut self.buffer[self.buffer_len..56] {
*b = 0;
}
let bit_len = self.total_len.wrapping_mul(8);
self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
let block = self.buffer;
self.compress(&block);
let mut out = [0u8; 32];
for (i, &s) in self.state.iter().enumerate() {
out[i * 4..(i + 1) * 4].copy_from_slice(&s.to_be_bytes());
}
out
}
fn compress(&mut self, block: &[u8]) {
let mut w = [0u32; 64];
for i in 0..16 {
w[i] = u32::from_be_bytes([
block[i * 4],
block[i * 4 + 1],
block[i * 4 + 2],
block[i * 4 + 3],
]);
}
for i in 16..64 {
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16]
.wrapping_add(s0)
.wrapping_add(w[i - 7])
.wrapping_add(s1);
}
let mut a = self.state[0];
let mut b = self.state[1];
let mut c = self.state[2];
let mut d = self.state[3];
let mut e = self.state[4];
let mut f = self.state[5];
let mut g = self.state[6];
let mut h = self.state[7];
for (i, w_i) in w.iter().enumerate() {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let t1 = h
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(Self::K[i])
.wrapping_add(*w_i);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let mj = (a & b) ^ (a & c) ^ (b & c);
let t2 = s0.wrapping_add(mj);
h = g;
g = f;
f = e;
e = d.wrapping_add(t1);
d = c;
c = b;
b = a;
a = t1.wrapping_add(t2);
}
self.state[0] = self.state[0].wrapping_add(a);
self.state[1] = self.state[1].wrapping_add(b);
self.state[2] = self.state[2].wrapping_add(c);
self.state[3] = self.state[3].wrapping_add(d);
self.state[4] = self.state[4].wrapping_add(e);
self.state[5] = self.state[5].wrapping_add(f);
self.state[6] = self.state[6].wrapping_add(g);
self.state[7] = self.state[7].wrapping_add(h);
}
}
fn write_cache(cache_path: &std::path::Path, data: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut hasher = Sha256::new();
hasher.update(data);
let hash = hasher.finalize();
let hash_hex = hash
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
std::fs::write(cache_path, data)?;
let hash_path = cache_path.with_extension("bin.sha256");
let mut f = std::fs::File::create(hash_path)?;
f.write_all(hash_hex.as_bytes())?;
Ok(())
}
#[test]
fn returns_none_when_cache_does_not_exist() {
let tmp = std::env::temp_dir().join("cvkg_test_no_cache.bin");
let _ = std::fs::remove_file(&tmp);
let result = load_pipeline_cache_with_integrity_check(&tmp);
assert!(
matches!(result, Ok(None)),
"missing cache should yield Ok(None), got {result:?}"
);
}
#[test]
fn returns_data_when_sidecar_matches() {
let tmp = std::env::temp_dir().join("cvkg_test_good_cache.bin");
let data = b"pipeline cache blob with some bytes";
write_cache(&tmp, data).expect("failed to write test cache");
let result = load_pipeline_cache_with_integrity_check(&tmp);
match result {
Ok(Some(d)) => assert_eq!(d, data),
other => panic!("expected Ok(Some(data)), got {other:?}"),
}
let _ = std::fs::remove_file(&tmp);
let _ = std::fs::remove_file(tmp.with_extension("bin.sha256"));
}
#[test]
fn returns_err_when_sidecar_missing() {
let tmp = std::env::temp_dir().join("cvkg_test_no_sidecar.bin");
std::fs::write(&tmp, b"data without sidecar").expect("failed to write test file");
let result = load_pipeline_cache_with_integrity_check(&tmp);
assert!(result.is_err(), "missing sidecar must yield Err");
let msg = result.unwrap_err();
assert!(msg.contains("sidecar hash file missing"), "got: {msg}");
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn returns_err_when_sidecar_hash_mismatches() {
let tmp = std::env::temp_dir().join("cvkg_test_bad_hash.bin");
std::fs::write(&tmp, b"original data").expect("failed to write test file");
let hash_path = tmp.with_extension("bin.sha256");
std::fs::write(
&hash_path,
b"0000000000000000000000000000000000000000000000000000000000000000",
)
.expect("failed to write hash sidecar");
std::fs::write(&tmp, b"tampered data with extra bytes").expect("failed to write test file");
let result = load_pipeline_cache_with_integrity_check(&tmp);
assert!(result.is_err(), "tampered cache must yield Err");
let msg = result.unwrap_err();
assert!(msg.contains("hash mismatch"), "got: {msg}");
let _ = std::fs::remove_file(&tmp);
let _ = std::fs::remove_file(&hash_path);
}
#[test]
fn sha256_of_known_input() {
let result = compute_sha256(b"abc");
let hex = format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}\
{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}\
{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}\
{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
result[0],
result[1],
result[2],
result[3],
result[4],
result[5],
result[6],
result[7],
result[8],
result[9],
result[10],
result[11],
result[12],
result[13],
result[14],
result[15],
result[16],
result[17],
result[18],
result[19],
result[20],
result[21],
result[22],
result[23],
result[24],
result[25],
result[26],
result[27],
result[28],
result[29],
result[30],
result[31],
);
assert_eq!(
hex,
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
const MIN_SVG_CAPACITY: usize = 512;
const MIN_SVG_TREES_CAPACITY: usize = 512;
const MIN_TEXT_CAPACITY: usize = 8192;
#[allow(clippy::assertions_on_constants)]
#[test]
fn svg_cache_capacity_meets_benchmark() {
assert!(
MIN_SVG_CAPACITY >= 512,
"SVG cache must be >= 512 to cover 200+ brush strokes"
);
}
#[allow(clippy::assertions_on_constants)]
#[test]
fn svg_trees_capacity_meets_benchmark() {
assert!(
MIN_SVG_TREES_CAPACITY >= 512,
"SVG trees cache must be >= 512 to cover 150+ unique sprites"
);
}
#[allow(clippy::assertions_on_constants)]
#[test]
fn text_cache_capacity_meets_benchmark() {
assert!(
MIN_TEXT_CAPACITY >= 8192,
"Text cache must be >= 8192 for typical text-heavy UIs"
);
}
#[test]
fn high_quality_uses_msaa_4x() {
assert_eq!(QualityLevel::High.msaa_sample_count(), 4);
}
#[test]
fn medium_quality_uses_msaa_2x() {
assert_eq!(QualityLevel::Medium.msaa_sample_count(), 2);
}
#[test]
fn low_quality_disables_msaa() {
assert_eq!(QualityLevel::Low.msaa_sample_count(), 1);
}
#[test]
fn default_is_high() {
assert_eq!(QualityLevel::default(), QualityLevel::High);
}
#[test]
fn all_levels_produce_valid_sample_counts() {
for level in [QualityLevel::High, QualityLevel::Medium, QualityLevel::Low] {
let n = level.msaa_sample_count();
assert!(
[1, 2, 4, 8, 16].contains(&n),
"QualityLevel {level:?} produced invalid sample count {n}"
);
}
}
#[test]
fn empty_list_returns_safe_format() {
let result = GpuRenderer::select_best_surface_format(&[]);
assert!(
matches!(
result,
TextureFormat::Rgba8Unorm
| TextureFormat::Bgra8Unorm
| TextureFormat::Rgba8UnormSrgb
| TextureFormat::Bgra8UnormSrgb
),
"empty list should return a known-safe format, got {result:?}"
);
}
#[test]
fn prefers_hdr_format_when_available() {
let formats = [
TextureFormat::Rgba8UnormSrgb,
TextureFormat::Rgba16Float,
TextureFormat::Bgra8UnormSrgb,
];
let result = GpuRenderer::select_best_surface_format(&formats);
assert_eq!(result, TextureFormat::Rgba16Float);
}
#[test]
fn prefers_srgb_when_no_hdr() {
let formats = [
TextureFormat::Rgba8Unorm,
TextureFormat::Rgba8UnormSrgb,
TextureFormat::Bgra8UnormSrgb,
];
let result = GpuRenderer::select_best_surface_format(&formats);
assert!(
matches!(
result,
TextureFormat::Rgba8Unorm
| TextureFormat::Rgba8UnormSrgb
| TextureFormat::Bgra8UnormSrgb
),
"expected a sRGB or linear format, got {result:?}"
);
}
#[test]
fn falls_back_to_linear_for_mobile_gpu() {
let formats = [TextureFormat::Rgba8Unorm, TextureFormat::Bgra8Unorm];
let result = GpuRenderer::select_best_surface_format(&formats);
assert!(
formats.contains(&result),
"mobile GPU should get a linear format from the list, got {result:?}"
);
}
#[test]
fn exotic_formats_fall_back_safely() {
let formats = [TextureFormat::Rgb9e5Ufloat];
let result = GpuRenderer::select_best_surface_format(&formats);
assert_eq!(result, TextureFormat::Rgb9e5Ufloat);
}
fn compute_ring_buffer_write(
write_start: usize,
write_count: usize,
max: usize,
) -> (usize, usize, usize) {
let effective_count = write_count.min(max);
let _drop_count = write_count - effective_count;
let first_chunk = (max - write_start).min(effective_count);
if first_chunk < effective_count {
let remaining = effective_count - first_chunk;
(first_chunk, remaining, remaining)
} else {
(first_chunk, 0, (write_start + effective_count) % max)
}
}
#[test]
fn no_wrap_no_overflow() {
let (first, second, head) = compute_ring_buffer_write(0, 10, 100);
assert_eq!(first, 10);
assert_eq!(second, 0);
assert_eq!(head, 10);
}
#[test]
fn wrap_without_overflow() {
let (first, second, head) = compute_ring_buffer_write(80, 50, 100);
assert_eq!(first, 20); assert_eq!(second, 30); assert_eq!(head, 30);
}
#[test]
fn overflow_caps_to_max() {
let (first, second, head) = compute_ring_buffer_write(80, 200, 100);
assert_eq!(first, 20);
assert_eq!(second, 80);
assert_eq!(head, 80);
}
#[test]
fn overflow_at_offset_zero() {
let (first, second, head) = compute_ring_buffer_write(0, 150, 100);
assert_eq!(first, 100);
assert_eq!(second, 0);
assert_eq!(head, 0); }
#[test]
fn empty_write() {
let (first, second, head) = compute_ring_buffer_write(50, 0, 100);
assert_eq!(first, 0);
assert_eq!(second, 0);
assert_eq!(head, 50);
}
#[test]
fn default_has_p1_5_cache_sizes() {
let cfg = RendererConfig::default();
assert_eq!(cfg.text_cache_capacity.get(), 8192);
assert_eq!(cfg.svg_cache_capacity.get(), 512);
assert_eq!(cfg.svg_trees_capacity.get(), 512);
assert_eq!(cfg.shared_elements_capacity.get(), 1024);
assert_eq!(cfg.image_uv_capacity.get(), 256);
assert_eq!(cfg.texture_registry_capacity.get(), 31);
assert_eq!(cfg.mega_heim_width, 4096);
assert_eq!(cfg.mega_heim_height, 4096);
}
#[test]
fn low_vram_uses_smaller_atlas() {
let cfg = RendererConfig::low_vram();
assert_eq!(cfg.mega_heim_width, 2048);
assert_eq!(cfg.mega_heim_height, 2048);
assert!(
cfg.mega_heim_vram_bytes() < 32 * 1024 * 1024,
"low_vram atlas should fit in 32MB, got {} bytes",
cfg.mega_heim_vram_bytes()
);
}
#[test]
fn high_end_uses_larger_atlas() {
let cfg = RendererConfig::high_end();
assert_eq!(cfg.mega_heim_width, 8192);
assert_eq!(cfg.mega_heim_height, 8192);
assert!(cfg.mega_heim_vram_bytes() >= 256 * 1024 * 1024);
}
#[test]
fn mega_heim_vram_is_4_bytes_per_pixel() {
let cfg = RendererConfig::default();
let expected = 4096u64 * 4096 * 4;
assert_eq!(cfg.mega_heim_vram_bytes(), expected);
}
#[test]
fn all_presets_have_nonzero_capacities() {
for (name, cfg) in [
("default", RendererConfig::default()),
("low_vram", RendererConfig::low_vram()),
("high_end", RendererConfig::high_end()),
] {
assert!(cfg.text_cache_capacity.get() > 0, "{name} text_cache");
assert!(cfg.svg_cache_capacity.get() > 0, "{name} svg_cache");
assert!(cfg.svg_trees_capacity.get() > 0, "{name} svg_trees");
assert!(
cfg.shared_elements_capacity.get() > 0,
"{name} shared_elements"
);
assert!(cfg.image_uv_capacity.get() > 0, "{name} image_uv");
assert!(
cfg.texture_registry_capacity.get() > 0,
"{name} texture_registry"
);
assert!(cfg.mega_heim_width > 0, "{name} mega_heim_width");
assert!(cfg.mega_heim_height > 0, "{name} mega_heim_height");
}
}
#[test]
fn config_is_cloneable_and_debug() {
let cfg = RendererConfig::default();
let _cloned = cfg.clone();
let _formatted = format!("{cfg:?}");
}
#[test]
fn p1_19_text_subsystem_shaped_cache_clearable() {
let mut subsystem =
crate::types::TextSubsystem::forge(std::num::NonZeroUsize::new(100).unwrap());
assert!(subsystem.shaped_cache.is_empty());
subsystem.shaped_cache.clear();
assert!(subsystem.shaped_cache.is_empty());
}
#[test]
fn p1_19_svg_subsystem_filter_batches_clearable() {
fn _has_clear_method(s: &mut crate::types::SvgSubsystem) {
s.clear_filter_batches();
}
}
#[test]
fn volumetric_wgsl_has_depth_bindings() {
let source = include_str!("../shaders/volumetric.wgsl");
assert!(
source.contains("depth_texture: texture_depth_2d"),
"volumetric.wgsl must declare single-sample depth texture binding"
);
assert!(
source.contains("depth_texture_msaa: texture_depth_multisampled_2d"),
"volumetric.wgsl must declare multisampled depth texture binding"
);
assert!(
source.contains("depth_sampler: sampler_comparison"),
"volumetric.wgsl must declare comparison sampler binding"
);
}
#[test]
fn volumetric_wgsl_reads_depth_for_occlusion() {
let source = include_str!("../shaders/volumetric.wgsl");
assert!(
source.contains("scene_depth"),
"volumetric.wgsl must read scene depth for occlusion"
);
assert!(
source.contains("msaa_count"),
"volumetric.wgsl must use msaa_count to select depth texture"
);
}
#[test]
fn volumetric_uniforms_has_msaa_count() {
let source = include_str!("../shaders/volumetric.wgsl");
assert!(
source.contains("msaa_count: f32"),
"VolumetricUniforms must have msaa_count field"
);
}
#[test]
fn depth_texture_usage_includes_texture_binding() {
let usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
assert!(usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
assert!(usage.contains(wgpu::TextureUsages::TEXTURE_BINDING));
}
#[test]
fn from_external_method_signature_compiles() {
fn assert_exists<F, T>(_: F)
where
F: FnOnce(
std::sync::Arc<wgpu::Device>,
std::sync::Arc<wgpu::Queue>,
wgpu::Surface<'static>,
u32,
u32,
) -> T,
{
}
assert_exists::<_, _>(GpuRenderer::from_external);
}