Use symmetric HexTile cell coordinates on CPU and GPU

Keep the existing regression and tolerance while preserving upstream
WireRemoval and test-service recovery changes.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
sanyamsingh001
2026-10-18 21:48:26 +00:00
co-authored by Codex
130 changed files with 6748 additions and 448 deletions
+6 -1
View File
@@ -320,7 +320,12 @@ fn pack(s: &Scene) -> [Vec<u8>; 7] {
let lit = !(s.lights.is_empty() && s.env.is_none() && s.ambient == [0.0; 3]);
globals.extend([e.rotation, lit as u32 as f32, 0.0, 0.0]);
let mats = s.materials.iter().flat_map(|m| {
let flags = (m.double_sided as u32) | (m.unlit as u32) << 1 | (m.accepts_lights as u32) << 2 | (m.receives_shadows as u32) << 3 | (m.alpha_mode << 8);
let flags = (m.double_sided as u32)
| (m.unlit as u32) << 1
| (m.accepts_lights as u32) << 2
| (m.receives_shadows as u32) << 3
| (m.shadows_only as u32) << 4
| (m.alpha_mode << 8);
[
m.base[0],
m.base[1],
+151 -8
View File
@@ -140,6 +140,74 @@ pub struct TransferStats {
pub readback_bytes: u64,
}
/// `WGPU_ADAPTER_NAME` trimmed; empty or whitespace-only counts as unset (an empty
/// substring would otherwise match the first adapter and skip the power preference).
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn wanted_adapter_name(var: Option<&str>) -> Option<&str> {
var.map(str::trim).filter(|w| !w.is_empty())
}
/// Index of the first adapter whose name contains `want`, compared case-insensitively.
/// Only the name is matched, as in wgpu (the backend is chosen with `WGPU_BACKEND`).
/// `Err` lists each distinct adapter once as `name (backend)`, so a typo or an
/// OS/driver-specific name is easy to spot.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
fn match_adapter_name(want: &str, adapters: &[(String, String)]) -> Result<usize, String> {
if adapters.is_empty() {
return Err(format!("WGPU_ADAPTER_NAME={want:?}: no adapters on the enabled backends"));
}
let w = want.to_lowercase();
adapters.iter().position(|(n, _)| n.to_lowercase().contains(&w)).ok_or_else(|| {
let mut list: Vec<String> = Vec::new();
for l in adapters.iter().map(|(n, b)| format!("{n} ({b})")) {
if !list.contains(&l) {
list.push(l);
}
}
format!(
"no adapter name matches WGPU_ADAPTER_NAME={want:?} (matched against the name only; \
select a backend with WGPU_BACKEND); available: [{}]",
list.join(", ")
)
})
}
/// The adapter named by `WGPU_ADAPTER_NAME` (case-insensitive substring, like wgpu's
/// `initialize_adapter_from_env`, which panics when none matches). `None` when unset or empty.
#[cfg(not(target_arch = "wasm32"))]
async fn adapter_by_name(instance: &wgpu::Instance) -> Option<Result<wgpu::Adapter, String>> {
let var = std::env::var("WGPU_ADAPTER_NAME").ok();
let want = wanted_adapter_name(var.as_deref())?;
let mut all = instance.enumerate_adapters(wgpu::Backends::all()).await;
let adapters: Vec<(String, String)> = all
.iter()
.map(|a| {
let i = a.get_info();
(i.name, format!("{:?}", i.backend))
})
.collect();
Some(match_adapter_name(want, &adapters).map(|i| all.swap_remove(i)))
}
/// `WGPU_POWER_PREF` (`low` / `high` / `none`), default HighPerformance. wgpu ignores an
/// unknown value silently; warn so a typo is noticed.
#[cfg(not(target_arch = "wasm32"))]
fn power_pref_from_env() -> wgpu::PowerPreference {
wgpu::PowerPreference::from_env().unwrap_or_else(|| {
if let Ok(v) = std::env::var("WGPU_POWER_PREF") {
log::warn!("gpu: ignoring WGPU_POWER_PREF={v:?} (expected low, high or none)");
}
wgpu::PowerPreference::HighPerformance
})
}
#[cfg(target_arch = "wasm32")]
fn power_pref_from_env() -> wgpu::PowerPreference {
wgpu::PowerPreference::HighPerformance
}
#[cfg(target_arch = "wasm32")]
async fn adapter_by_name(_: &wgpu::Instance) -> Option<Result<wgpu::Adapter, String>> {
None
}
impl Transfers {
fn count(&self, up: bool, bytes: usize) {
use std::sync::atomic::Ordering::Relaxed;
@@ -774,19 +842,28 @@ impl GpuContext {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
#[cfg(not(target_arch = "wasm32"))]
{
// Match eframe's backend override so CLI and headless acceptance tests
// exercise the backend requested for the native viewer.
desc.backends = wgpu::Backends::from_env().unwrap_or(desc.backends);
// Honour the same wgpu env vars as the viewer (WGPU_BACKEND, WGPU_DX12_COMPILER,
// WGPU_VALIDATION, WGPU_DEBUG, WGPU_POWER_PREF), plus WGPU_ADAPTER_NAME (CLI/headless
// only for now; the viewer ignores it). Not full parity: the viewer also changes
// instance flags and has a GL fallback.
// Note: with_env() also resets memory_budget_thresholds to their default (unused here).
desc = desc.with_env();
}
#[cfg(target_arch = "wasm32")]
{
desc.backends = wgpu::Backends::BROWSER_WEBGPU;
}
let instance = wgpu::Instance::new(desc);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, ..Default::default() })
.await
.map_err(|e| format!("no adapter: {e}"))?;
let (adapter, by_name) = match adapter_by_name(&instance).await {
Some(a) => (a?, true),
None => (
instance
.request_adapter(&wgpu::RequestAdapterOptions { power_preference: power_pref_from_env(), ..Default::default() })
.await
.map_err(|e| format!("no adapter: {e}"))?,
false,
),
};
let limits = adapter.limits();
let required_limits = wgpu::Limits {
max_texture_dimension_2d: limits.max_texture_dimension_2d.min(16384),
@@ -796,7 +873,10 @@ impl GpuContext {
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor { label: Some("effectcraft gpu"), required_limits, ..Default::default() })
.await
.map_err(|e| format!("no device: {e}"))?;
.map_err(|e| match by_name {
true => format!("no device on {} (picked by WGPU_ADAPTER_NAME): {e}", adapter.get_info().name),
false => format!("no device: {e}"),
})?;
let context = GpuContext::new(&adapter, device, queue)?;
// This path owns the device; install handlers only after construction succeeds.
crate::readback::install(&context.device, &context.readbacks);
@@ -1392,3 +1472,66 @@ impl<'g> Enc<'g> {
pub(crate) fn mip_levels(w: u32, h: u32) -> u32 {
u32::BITS - w.max(h).max(1).leading_zeros()
}
#[cfg(test)]
mod adapter_name_tests {
use super::{match_adapter_name, wanted_adapter_name};
fn adapters() -> Vec<(String, String)> {
[("NVIDIA GeForce GTX 1070", "Vulkan"), ("Intel(R) UHD Graphics 630", "Dx12"), ("llvmpipe (LLVM 19.1.7, 256 bits)", "Gl")]
.map(|(n, b)| (n.into(), b.into()))
.to_vec()
}
#[test]
fn matches_case_insensitive_substring() {
assert_eq!(match_adapter_name("intel", &adapters()), Ok(1));
assert_eq!(match_adapter_name("GTX 1070", &adapters()), Ok(0));
assert_eq!(match_adapter_name("LLVMPIPE", &adapters()), Ok(2));
}
#[test]
fn first_match_wins() {
assert_eq!(match_adapter_name("i", &adapters()), Ok(0));
}
#[test]
fn empty_or_blank_name_is_unset_and_value_is_trimmed() {
assert_eq!(wanted_adapter_name(None), None);
assert_eq!(wanted_adapter_name(Some("")), None);
assert_eq!(wanted_adapter_name(Some(" ")), None);
assert_eq!(wanted_adapter_name(Some(" intel ")), Some("intel"));
assert_eq!(match_adapter_name(wanted_adapter_name(Some(" intel ")).unwrap(), &adapters()), Ok(1));
}
#[test]
fn mismatch_lists_name_and_backend() {
let e = match_adapter_name("NoSuch", &adapters()).unwrap_err();
assert!(e.contains("\"NoSuch\""), "{e}");
assert!(e.contains("Intel(R) UHD Graphics 630 (Dx12)"), "{e}");
assert!(e.contains("NVIDIA GeForce GTX 1070 (Vulkan)"), "{e}");
}
#[test]
fn backend_is_not_part_of_the_match_and_message_says_so() {
let e = match_adapter_name("dx12", &adapters()).unwrap_err();
assert!(e.contains("name only"), "{e}");
assert!(e.contains("WGPU_BACKEND"), "{e}");
}
#[test]
fn mismatch_lists_identical_name_and_backend_once() {
let mut a = adapters();
a.insert(2, ("Intel(R) UHD Graphics 630".into(), "Dx12".into()));
a.push(("Intel(R) UHD Graphics 630".into(), "Vulkan".into()));
let e = match_adapter_name("NoSuch", &a).unwrap_err();
assert_eq!(e.matches("Intel(R) UHD Graphics 630 (Dx12)").count(), 1, "{e}");
assert_eq!(e.matches("Intel(R) UHD Graphics 630 (Vulkan)").count(), 1, "{e}");
}
#[test]
fn no_adapters_has_its_own_message() {
let e = match_adapter_name("x", &[]).unwrap_err();
assert!(e.contains("no adapters on the enabled backends"), "{e}");
}
}
+4 -1
View File
@@ -111,7 +111,10 @@ fn wave_warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let dir = ctx.params.f("direction").to_radians();
let speed = ctx.params.f("speed");
let phase = ctx.params.f("phase").to_radians() + ctx.time * speed * TAU;
let (dx, dy) = (dir.sin(), -dir.cos());
// Cardinal directions must not acquire a tiny perpendicular slope: at a square-wave
// boundary cos(90°)'s rounding residue can flip a whole column on CPU but not GPU.
let axis = |v: f64| if v.abs() < 4.0 * f64::EPSILON { 0.0 } else { v };
let (dx, dy) = (axis(dir.sin()), -axis(dir.cos()));
if !ctx.adjustment {
b.pad(e, h.abs().ceil() as u32 + 1)?;
}
+1 -1
View File
@@ -493,7 +493,7 @@ fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let mut p = Params::default();
p.u[0][0] = ctx.params.e("removalStyle");
p.f[0] = [a.0 as f32, a.1 as f32, -ty as f32, tx as f32];
p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, 0.0];
p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, len as f32];
run(e, "fp2_wire", &p, b, None, None)
}
+55 -16
View File
@@ -288,6 +288,53 @@ struct FOut {
@location(1) depth: u32,
};
// shade::to_light: unit direction to the light (xyz) and its distance (w).
fn to_light(kind: u32, lpos: vec3<f32>, ldir: vec3<f32>, pos: vec3<f32>) -> vec4<f32> {
if (kind == 0u) {
return vec4<f32>(-ldir, 1e30);
}
let d = lpos - pos;
let len = sqrt(dot(d, d));
if (len < 1e-6) {
return vec4<f32>(0.0, 0.0, -1.0, 0.0);
}
return vec4<f32>(d / len, len);
}
// shade::shadow_only: the strongest shadow any light casts on the fragment.
fn shadow_only_amount(unlit: bool, normal: vec3<f32>, front: bool, pos: vec3<f32>) -> f32 {
var amount = 0.0;
if (unlit) {
return amount;
}
var n = norm3(normal);
if (!front) {
n = -n;
}
let nl_count = u32(g.fwd.w);
for (var li = 0u; li < nl_count; li = li + 1u) {
let lb = li * LIGHT_STRIDE;
let l0 = lights[lb];
let l1 = lights[lb + 1u];
let l2 = lights[lb + 2u];
let l3 = lights[lb + 3u];
let l4 = lights[lb + 4u];
let kind = u32(l0.w);
let tl = to_light(kind, l0.xyz, l1.xyz, pos);
let to_l = tl.xyz;
let dist = tl.w;
if (attenuation(kind, u32(l3.x), l3.y, l3.z, l2.w, l1.w, l1.xyz, to_l, dist) <= 0.0) {
continue;
}
let n_l = dot(n, to_l);
if (n_l <= 0.0) {
continue;
}
amount = max(amount, 1.0 - light_shadow(li, kind, l0.xyz, i32(l3.w), l4.x, pos, n_l));
}
return amount;
}
@fragment
fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
let mb = i.material * MAT_STRIDE;
@@ -302,6 +349,7 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
let unlit = (flags & 2u) != 0u;
let accepts_lights = (flags & 4u) != 0u;
let receives_shadows = (flags & 8u) != 0u;
let shadows_only = (flags & 16u) != 0u;
let alpha_mode = (flags >> 8u) & 3u;
if (!front && !double_sided) {
discard;
@@ -330,6 +378,10 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
}
var out: FOut;
out.depth = bitcast<u32>(dot(g.view_z.xyz, i.pos) + g.view_z.w);
if (shadows_only) {
out.color = vec4<f32>(0.0, 0.0, 0.0, alpha * shadow_only_amount(unlit || !accepts_lights, i.normal, front, i.pos));
return out;
}
if (unlit || !accepts_lights || g.env2.y < 0.5) {
let c = rgb + emissive;
out.color = vec4<f32>(c * alpha, alpha);
@@ -381,22 +433,9 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
let l3 = lights[lb + 3u];
let l4 = lights[lb + 4u];
let kind = u32(l0.w);
var to_l: vec3<f32>;
var dist: f32;
if (kind == 0u) {
to_l = -l1.xyz;
dist = 1e30;
} else {
let d = l0.xyz - i.pos;
let len = sqrt(dot(d, d));
if (len < 1e-6) {
to_l = vec3<f32>(0.0, 0.0, -1.0);
dist = 0.0;
} else {
to_l = d / len;
dist = len;
}
}
let tl = to_light(kind, l0.xyz, l1.xyz, i.pos);
let to_l = tl.xyz;
let dist = tl.w;
let k = attenuation(kind, u32(l3.x), l3.y, l3.z, l2.w, l1.w, l1.xyz, to_l, dist);
if (k <= 0.0) {
continue;
+4 -2
View File
@@ -830,7 +830,7 @@ fn fp2_io_final(@builtin(global_invocation_id) gid: vec3<u32>) {
}
// ---------------------------------------------------------------- CC Simple Wire Removal (keying2::wire_removal)
// u[0].x = style; f[0] = (ax, ay, nx, ny); f[1] = (hw thickness, soft width, mirror).
// u[0].x = style; f[0] = (ax, ay, nx, ny); f[1] = (hw thickness, soft width, mirror, segment length).
fn fp2_wire_pos(base: vec2<f32>, nrm: vec2<f32>, s: f32) -> vec4<f32> {
return sample_bilinear_clamped(src, base.x + nrm.x * s, base.y + nrm.y * s);
@@ -853,7 +853,9 @@ fn fp2_wire(@builtin(global_invocation_id) gid: vec3<u32>) {
let q = vec2<f32>(fx - a.x, fy - a.y);
let d = q.x * nrm.x + q.y * nrm.y;
let ad = abs(d);
if (ad > hw + soft_w) {
// The wire is the segment A to B: the tangent is the normal turned back by 90 degrees.
let along = q.x * nrm.y - q.y * nrm.x;
if (along < 0.0 || along > P.f[1].w || ad > hw + soft_w) {
textureStore(out, p, px);
return;
}
+12
View File
@@ -155,6 +155,18 @@ fn gpu_matches_cpu_unlit_and_full_frames() {
assert!((diff as f64) < 0.02 * cpu.data.len() as f64, "{diff} pixels differ");
}
#[test]
fn gpu_matches_cpu_shadow_catcher() {
// #1691: the floor plane as a shadow catcher ("Accepts Shadows: Only").
let Some(g) = gpu() else { return };
let (mut p, cid) = scene_project();
let floor = p.comp_mut(cid).unwrap().layers.last_mut().unwrap();
set(floor, "materialOptions/acceptsShadows", Value::Enum(2));
let s = scene(&p, cid);
assert!(s.materials.iter().any(|m| m.shadows_only) && !s.shadows.is_empty());
compare(&s, &g);
}
#[test]
fn gpu_raster_perf_smoke() {
let Some(g) = gpu() else { return };
+8
View File
@@ -28,6 +28,11 @@ fn twirl_bulge_ripple_wave_warp() {
effect_case("ec.distort.wavewarp", &[("waveType", e(2)), ("pinning", e(1)), ("phase", n(400.0)), ("speed", n(-2.0))]);
effect_case("ec.distort.wavewarp", &[("waveType", e(8)), ("pinning", e(2)), ("height", n(-5.0)), ("direction", n(200.0))]);
effect_case("ec.distort.wavewarp", &[("waveType", e(4)), ("pinning", e(7)), ("width", n(33.0))]);
// #1181: the single-edge pinning paths, with the default vertical displacement direction.
effect_case("ec.distort.wavewarp", &[("direction", n(90.0)), ("height", n(7.0)), ("width", n(25.0)), ("pinning", e(4))]);
effect_case("ec.distort.wavewarp", &[("direction", n(90.0)), ("height", n(7.0)), ("width", n(19.0)), ("pinning", e(6)), ("waveType", e(1))]);
effect_case("ec.distort.wavewarp", &[("direction", n(45.0)), ("height", n(-6.0)), ("pinning", e(4)), ("speed", n(2.0))]);
effect_case("ec.distort.wavewarp", &[("direction", n(45.0)), ("height", n(-6.0)), ("pinning", e(6)), ("speed", n(2.0))]);
}
#[test]
@@ -79,6 +84,9 @@ fn shape_case(id: &str, vals: &[(&str, Value)]) {
fn edge_pinning_on_a_shape_layer() {
shape_case("ec.distort.turbulentdisplace", &[("size", n(30.0)), ("amount", n(20.0))]);
shape_case("ec.distort.wavewarp", &[("pinning", e(1)), ("height", n(6.0)), ("width", n(25.0))]);
// #1181: single-edge pinning over the layer rect, not just All Edges.
shape_case("ec.distort.wavewarp", &[("pinning", e(4)), ("direction", n(90.0)), ("height", n(6.0)), ("width", n(25.0))]);
shape_case("ec.distort.wavewarp", &[("pinning", e(6)), ("direction", n(90.0)), ("height", n(6.0)), ("width", n(25.0))]);
shape_case(
"ec.distort.bulge",
&[("pinAllEdges", on()), ("height", n(2.0)), ("hradius", n(40.0)), ("vradius", n(30.0)), ("center", Value::Vec2([28.5, 20.5]))],