facett-map/tests/rail_dash_pixels.rs
//! **A RAIL'S CORE IS DASHED ALONG ITS ARC LENGTH, ON BOTH LANES** — HO-T3, the dash lane
//! GG #28 measured absent (*"`LineVertex` is 48 B with no arc length, `line.wgsl` has no
//! dash term"*).
//!
//! # The law under test
//!
//! `style::dash_period_px`: a dashed way's core alternates lit / casing with a period of
//! `DASH_PERIOD_RATIO · w_px` **along the way**, `w_px` being the one resolved width both
//! lanes already agree on (GG #27). "Along the way" is the whole claim — a period taken
//! from screen `x` would give a 45° rail `1/√2` as many dashes as a horizontal one of the
//! same length, and a period taken from the pane would crawl under every pan.
//!
//! # What is measured
//!
//! Real device pixels through the shipped `LineRenderer` (casing pass + fill pass, the
//! same two draws the live lane submits) on geometry baked by `build_gpu_geometry` from a
//! `Way` whose kind is [`WayKind::Rail`] — so the KIND gate is under test, not a flag the
//! test set itself. The centreline is walked at 1 px steps and each sample classified lit
//! (the core ink) or dark (the casing showing through); the maximal lit runs are counted.
//!
//! 1. **a horizontal rail** carries `ceil(L_px / period)` lit runs (the way starts lit);
//! 2. **a 45° rail of the SAME Mercator length** carries the same number — arc length, not
//! screen x;
//! 3. **a road of the same geometry** carries exactly one run — dash off;
//! 4. **the same rail panned by a fractional pixel offset** puts every run start within
//! 1.5 px of where it was, measured along the way — no crawling.
//!
//! Then the CPU painter, through the `egui_kittest` harness `rail_tier_pixels.rs` uses:
//! the row through a horizontal rail's band alternates the rail ink with the casing ink
//! at the same period, `dash_period_px(cpu width)`.
//!
//! # THE RED
//!
//! Before the dash lane: `a_rail_is_dashed_along_its_arc_length_on_the_gpu_lane` —
//! `the horizontal rail carries 1 lit run(s) along 400 px; period 48 px says 9` — the core
//! was one uniform stroke on every sample. The CPU arm read 1 run the same way.
//!
//! Run: `env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=integrated cargo test -p facett-map
//! --features wgpu --test rail_dash_pixels -- --test-threads=1 --nocapture`
#![cfg(feature = "wgpu")]
use facett_map::gpu::line::LineRenderer;
use facett_map::gpu::{build_gpu_geometry, ways_origin, LineVertex, MapTransform};
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::style::{dash_period_px, DASH_DUTY, DASH_PERIOD_RATIO};
use facett_map::{Way, WayKind};
mod common;
const W: u32 = 512;
const H: u32 = 512;
/// The on-screen length every GPU fixture is fitted to.
const L_PX: f32 = 400.0;
/// Themed width, bumped like `rail_tier_pixels.rs` bumps its rail's so the core is wider
/// than the AA feather and a centreline sample reads PURE ink.
const STYLE_W: f32 = 12.0;
const FEATHER_PX: f32 = 1.0;
/// The core ink the fixtures are recoloured to — bright, so lit/dark is a luma verdict.
const CORE: egui::Color32 = egui::Color32::from_rgb(0xF0, 0xF0, 0xE0);
const CASING: [f32; 4] = [0.10, 0.11, 0.14, 1.0];
/// A mid-dark paper, far from the core in luma and never on the centreline anyway.
const PAPER: [f64; 4] = [0.20, 0.35, 0.55, 1.0];
const START: (f64, f64) = (18.00, 59.32);
/// A straight way of Mercator length `len` from `START` in direction `(dx, dy)` (unit,
/// Mercator), `n` points, as `kind`. Points are spaced in MERCATOR so the two fixtures
/// carry the same arc length to f64 precision.
fn straight(id: i64, dir: [f64; 2], len: f64, tags: &[(&str, &str)], pal: &facett_map::MapPalette) -> Way {
let (mx, my) = facett_map::mercator(START.0, START.1);
let n = 11;
let pts: Vec<(f64, f64)> = (0..n)
.map(|i| {
let t = f64::from(i) / f64::from(n - 1);
facett_map::unmercator(mx + dir[0] * len * t, my + dir[1] * len * t)
})
.collect();
let mut w = Way::from_lonlat_with(id, &pts, tags, pal).expect("a straight way");
w.style.width = STYLE_W;
w.style.color = CORE;
w
}
/// Mercator length of the horizontal fixture: 0.2° of longitude.
fn fixture_len() -> f64 {
let (a, _) = facett_map::mercator(START.0, START.1);
let (b, _) = facett_map::mercator(START.0 + 0.2, START.1);
b - a
}
const RAIL: &[(&str, &str)] = &[("railway", "rail")];
const ROAD: &[(&str, &str)] = &[("highway", "primary")];
/// The transform that puts `way`'s midpoint at the pane centre (+ `pan` px) and fits its
/// length to `L_PX` — north-up, flat, uniform zoom.
fn fit(way: &Way, origin: [f64; 2], pan: [f32; 2]) -> MapTransform {
let zoom = L_PX / fixture_len() as f32;
let (a, b) = (way.lonlat[0], way.lonlat[way.lonlat.len() - 1]);
let (ax, ay) = facett_map::mercator(a.0, a.1);
let (bx, by) = facett_map::mercator(b.0, b.1);
MapTransform {
zoom_x: zoom,
zoom_y: zoom,
ref_x: (0.5 * (ax + bx) - origin[0]) as f32,
ref_y: (0.5 * (ay + by) - origin[1]) as f32,
screen_center_x: W as f32 * 0.5 + pan[0],
screen_center_y: H as f32 * 0.5 + pan[1],
bearing_deg: 0.0,
tilt_deg: 0.0,
}
}
/// Render `verts` through the shipped two-pass line renderer into an RGBA8 image. The
/// same attachment recipe as `line_gpu.rs` (4× MSAA colour + depth, resolved into a
/// single-sample readback target).
fn render(device: &wgpu::Device, queue: &wgpu::Queue, verts: &[LineVertex], t: &MapTransform) -> image::RgbaImage {
use wgpu::util::DeviceExt;
let format = wgpu::TextureFormat::Rgba8Unorm;
let renderer = LineRenderer::new(device, format);
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("rail_dash_verts"),
contents: bytemuck::cast_slice(verts),
usage: wgpu::BufferUsages::VERTEX,
});
renderer.write_uniforms(queue, t, W as f32, H as f32, 3.0, CASING, FEATHER_PX, 1.0);
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rail_dash_target"),
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: facett_core::render::gpu::NO_MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let msaa = device
.create_texture(&wgpu::TextureDescriptor {
label: Some("rail_dash_msaa"),
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
.create_view(&wgpu::TextureViewDescriptor::default());
let depth = device
.create_texture(&wgpu::TextureDescriptor {
label: Some("rail_dash_depth"),
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: facett_map::gpu::MAP_DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
.create_view(&wgpu::TextureViewDescriptor::default());
let bpp = 4u32;
let padded = (W * bpp).div_ceil(256) * 256;
let read_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rail_dash_readback"),
size: u64::from(padded * H),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("rail_dash_enc") });
{
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rail_dash_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &msaa,
depth_slice: None,
resolve_target: Some(&view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: PAPER[0], g: PAPER[1], b: PAPER[2], a: PAPER[3] }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
renderer.draw(&mut pass, &vbuf, verts.len() as u32);
}
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyBufferInfo {
buffer: &read_buf,
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded), rows_per_image: Some(H) },
},
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
);
queue.submit([enc.finish()]);
let slice = read_buf.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
device.poll(wgpu::PollType::wait_indefinitely()).ok();
let data = slice.get_mapped_range().expect("wgpu: get_mapped_range on a mapped buffer");
let mut img = image::RgbaImage::new(W, H);
for y in 0..H {
for x in 0..W {
let o = (y * padded + x * bpp) as usize;
img.put_pixel(x, y, image::Rgba([data[o], data[o + 1], data[o + 2], data[o + 3]]));
}
}
img
}
fn luma(p: &image::Rgba<u8>) -> f32 {
0.2126 * f32::from(p[0]) + 0.7152 * f32::from(p[1]) + 0.0722 * f32::from(p[2])
}
/// Lit (`Some(true)`), dark (`Some(false)`), or an AA blend nobody should vote on.
fn classify(p: &image::Rgba<u8>) -> Option<bool> {
let l = luma(p);
if l >= 160.0 {
Some(true)
} else if l <= 100.0 {
Some(false)
} else {
None
}
}
/// Walk `way`'s centreline at 1 px steps under `t`, 8 px in from each end so the round
/// caps stay out of the census, and return the arc position (px from the way's start) at
/// which each maximal LIT run begins.
fn lit_run_starts(img: &image::RgbaImage, way: &Way, origin: [f64; 2], t: &MapTransform) -> Vec<f32> {
let (a, b) = (way.lonlat[0], way.lonlat[way.lonlat.len() - 1]);
let (ax, ay) = facett_map::mercator(a.0, a.1);
let (bx, by) = facett_map::mercator(b.0, b.1);
let pa = t.project([(ax - origin[0]) as f32, (ay - origin[1]) as f32]);
let pb = t.project([(bx - origin[0]) as f32, (by - origin[1]) as f32]);
let len = ((pb[0] - pa[0]).powi(2) + (pb[1] - pa[1]).powi(2)).sqrt();
let mut starts = Vec::new();
let mut prev: Option<bool> = None;
let mut s = 8.0f32;
while s <= len - 8.0 {
let f = s / len;
let x = pa[0] + (pb[0] - pa[0]) * f;
let y = pa[1] + (pb[1] - pa[1]) * f;
let (xi, yi) = (x.round() as i64, y.round() as i64);
assert!(xi >= 0 && yi >= 0 && xi < i64::from(W) && yi < i64::from(H), "the fixture left the pane at s={s}");
if let Some(lit) = classify(img.get_pixel(xi as u32, yi as u32)) {
if lit && prev != Some(true) {
starts.push(s);
}
prev = Some(lit);
}
s += 1.0;
}
starts
}
/// The number of lit runs the law predicts along `len_px` of a way that starts lit:
/// every `k · period < len` begins one.
fn predicted_runs(len_px: f32, period_px: f32) -> usize {
(0..).take_while(|k| *k as f32 * period_px < len_px).count()
}
fn bake(way: &Way, origin: [f64; 2]) -> Vec<LineVertex> {
build_gpu_geometry(std::iter::once((way, way.style.lod as u32)), origin).line_vertices
}
#[test]
fn a_rail_is_dashed_along_its_arc_length_on_the_gpu_lane() {
let Some((device, queue)) = common::device_or_skip(
"rail_dash_gpu",
"a WayKind::Rail core alternates lit/casing at dash_period_px along its arc length; a road does not",
) else {
return;
};
let pal = facett_map::MapPalette::cartographic(false);
let len = fixture_len();
let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
let horizontal = straight(1, [1.0, 0.0], len, RAIL, &pal);
let diagonal = straight(2, [inv_sqrt2, inv_sqrt2], len, RAIL, &pal);
let road = straight(3, [1.0, 0.0], len, ROAD, &pal);
assert_eq!(horizontal.style.kind, WayKind::Rail, "railway=rail classifies as the Rail kind");
assert_eq!(road.style.kind, WayKind::Road, "highway=primary classifies as a Road");
assert!(horizontal.style.dashed() && !road.style.dashed(), "the style gate: rail dashes, road does not");
let origin = ways_origin([&horizontal, &diagonal, &road].into_iter()).expect("an origin");
// width_scale is 1.0 in `render`, so the resolved width IS the themed width (in band).
let w_px = facett_map::style::line_width_px_scaled(STYLE_W, 1.0);
let period = dash_period_px(w_px);
let want = predicted_runs(L_PX, period);
let t_h = fit(&horizontal, origin, [0.0, 0.0]);
let img_h = render(&device, &queue, &bake(&horizontal, origin), &t_h);
let runs_h = lit_run_starts(&img_h, &horizontal, origin, &t_h);
eprintln!("[rail-dash] horizontal rail: {} lit runs at {:?} (period {period} px, want {want})", runs_h.len(), runs_h);
assert!(
(runs_h.len() as i64 - want as i64).abs() <= 1,
"the horizontal rail carries {} lit run(s) along {L_PX} px; period {period} px \
(= {DASH_PERIOD_RATIO} × {w_px} px width) says {want}. A uniform core reads 1 here.",
runs_h.len()
);
// Consecutive lit runs start one period apart. The FIRST run is skipped: the census
// begins 8 px in (past the round cap), so its recorded start is the inset, not 0.
for pair in runs_h[1..].windows(2) {
let gap = pair[1] - pair[0];
assert!((gap - period).abs() <= 2.0, "lit runs start {gap} px apart; the period is {period} px");
}
let t_d = fit(&diagonal, origin, [0.0, 0.0]);
let img_d = render(&device, &queue, &bake(&diagonal, origin), &t_d);
let runs_d = lit_run_starts(&img_d, &diagonal, origin, &t_d);
eprintln!("[rail-dash] diagonal rail: {} lit runs at {:?}", runs_d.len(), runs_d);
assert_eq!(
runs_d.len(),
runs_h.len(),
"a 45° rail of the same Mercator length carries {} lit runs where the horizontal one \
carries {} — a period taken from screen x, not arc length, reads 1/√2 as many",
runs_d.len(),
runs_h.len()
);
let img_r = render(&device, &queue, &bake(&road, origin), &t_h);
let runs_r = lit_run_starts(&img_r, &road, origin, &t_h);
assert_eq!(runs_r.len(), 1, "a road of the same geometry is ONE lit run, not {} — dash is off for roads", runs_r.len());
// ── NO CRAWLING: pan by a fractional offset, the dashes stay where the way is.
let t_p = fit(&horizontal, origin, [37.3, -21.7]);
let img_p = render(&device, &queue, &bake(&horizontal, origin), &t_p);
let runs_p = lit_run_starts(&img_p, &horizontal, origin, &t_p);
assert_eq!(runs_p.len(), runs_h.len(), "a pan changed the dash count {} → {}", runs_h.len(), runs_p.len());
for (a, b) in runs_h.iter().zip(&runs_p) {
assert!((a - b).abs() <= 1.5, "a lit run crawled under pan: started at {a} px along the way, now {b} px");
}
if std::env::var("FACETT_MAP_DUMP_PNG").is_ok() {
let dir = std::env::temp_dir();
img_h.save(dir.join("rail-dash-horizontal.png")).ok();
img_d.save(dir.join("rail-dash-diagonal.png")).ok();
img_r.save(dir.join("rail-dash-road.png")).ok();
}
}
/// **The WGSL twins of `DASH_PERIOD_RATIO` / `DASH_DUTY` are the Rust values**, read out
/// of the shader text the pipeline compiles — the arrangement `MIN_COVERAGE_ALPHA` and
/// `CASING_DEPTH_BIAS` already live under.
#[test]
fn the_shader_carries_the_dash_law_as_the_style_module_declares_it() {
let src = facett_map::gpu::line::LINE_WGSL;
let read = |name: &str| -> f32 {
let needle = format!("const {name}: f32 = ");
let at = src.find(&needle).unwrap_or_else(|| panic!("line.wgsl declares no `{needle}…;` — the dash law has no shader twin"));
let rest = &src[at + needle.len()..];
let end = rest.find(';').expect("a terminated const");
rest[..end].trim().parse::<f32>().unwrap_or_else(|e| panic!("`{name}` is not a literal: {e}"))
};
assert_eq!(read("DASH_PERIOD_RATIO"), DASH_PERIOD_RATIO, "line.wgsl's DASH_PERIOD_RATIO drifted from style.rs");
assert_eq!(read("DASH_DUTY"), DASH_DUTY, "line.wgsl's DASH_DUTY drifted from style.rs");
}
// ── The CPU painter ──────────────────────────────────────────────────────────
const CANVAS: egui::Vec2 = egui::vec2(900.0, 640.0);
const CPU_CAMERA: f32 = 12.0;
fn cpu_render(kind_tags: &[(&str, &str)]) -> (MapTheme, f32, image::RgbaImage) {
let theme = facett_core::Theme::default();
let map_theme = MapTheme::day(&theme);
let pal = map_theme.palette;
let lat = 59.32;
let pts: Vec<(f64, f64)> = (0..=20).map(|i| (17.90 + f64::from(i) * 0.02, lat)).collect();
let mut w = Way::from_lonlat_with(1, &pts, kind_tags, &pal).expect("a straight way");
w.style.width = STYLE_W;
let view = MapView::new(LatLon::new(lat, 18.10), CPU_CAMERA, (CANVAS.x, CANVAS.y));
let mut f = MapFacet::new("rail-dash-cpu", view, map_theme.clone());
let cpu_w = f.cpu_line_width_px_for_test(STYLE_W);
f.push_layer(Box::new(WaysLayer::new("net-rails", vec![w])));
let mut h = egui_kittest::Harness::builder().with_size(CANVAS).build_ui(move |ui| {
f.ui(ui);
});
h.run_steps(3);
(map_theme, cpu_w, h.render().expect("the CPU painter rendered"))
}
fn cheb(a: &image::Rgba<u8>, b: egui::Color32) -> i32 {
(i32::from(a[0]) - i32::from(b.r()))
.abs()
.max((i32::from(a[1]) - i32::from(b.g())).abs())
.max((i32::from(a[2]) - i32::from(b.b())).abs())
}
/// Along the row with the most `ink`, the maximal runs of `ink` (casing or anything else
/// between them).
fn ink_runs_on_best_row(img: &image::RgbaImage, ink: egui::Color32) -> (u32, usize) {
let mut best = (0u32, 0usize);
for y in 0..img.height() {
let n = (0..img.width()).filter(|&x| cheb(img.get_pixel(x, y), ink) <= 10).count();
if n > best.1 {
best = (y, n);
}
}
let y = best.0;
let mut runs = 0;
let mut prev = false;
for x in 0..img.width() {
let is_ink = cheb(img.get_pixel(x, y), ink) <= 10;
if is_ink && !prev {
runs += 1;
}
prev = is_ink;
}
(y, runs)
}
#[test]
fn the_cpu_painter_dashes_a_rail_at_the_same_period_and_leaves_a_road_solid() {
let _gpu = common::one_gpu::hold();
let (theme, cpu_w, rail) = cpu_render(RAIL);
let (_, _, road) = cpu_render(ROAD);
let ink = theme.palette.rail();
let period = dash_period_px(cpu_w);
// The way crosses the whole 900 px canvas, so the row carries ~canvas/period runs; the
// phase at the left edge is whatever the off-canvas start left it, hence ±2.
let want = (CANVAS.x / period).round() as i64;
let (row, runs) = ink_runs_on_best_row(&rail, ink);
eprintln!("[rail-dash] CPU rail row {row}: {runs} runs of {ink:?}; width {cpu_w} px, period {period} px, want ~{want}");
assert!(
(runs as i64 - want).abs() <= 2,
"the CPU painter drew the rail's core as {runs} run(s) across {} px; dash_period_px({cpu_w}) = {period} says ~{want}",
CANVAS.x
);
let road_ink = road_core_ink(&road);
let (_, road_runs) = ink_runs_on_best_row(&road, road_ink);
assert_eq!(road_runs, 1, "the CPU painter dashed a ROAD into {road_runs} runs");
}
/// The road's own ink, read off the picture: the most common non-land hue on the busiest
/// row, so the road test does not have to know which tier `highway=primary` resolves to.
fn road_core_ink(img: &image::RgbaImage) -> egui::Color32 {
let mut counts: std::collections::HashMap<[u8; 3], usize> = std::collections::HashMap::new();
for p in img.pixels() {
*counts.entry([p[0], p[1], p[2]]).or_default() += 1;
}
// Land is the majority; the road core is the runner-up among saturated-enough pixels.
let mut v: Vec<_> = counts.into_iter().collect();
v.sort_by_key(|e| std::cmp::Reverse(e.1));
let land = v[0].0;
let road = v.iter().skip(1).find(|(c, _)| c.iter().zip(land).map(|(a, b)| (i32::from(*a) - i32::from(b)).abs()).max().unwrap() > 20).expect("a road hue").0;
egui::Color32::from_rgb(road[0], road[1], road[2])
}
// ── The bench: 2 000 rail ways, wall clock per frame ─────────────────────────
/// **Frame time of the two-pass line lane over 2 000 rail ways** — the number HO-T3 asks
/// for before/after the dash term. Wall clock from `submit` to the fence, median of 40
/// frames after 5 warm-ups, no readback in the loop; the adapter is printed so a figure
/// cannot be quoted without its hardware.
#[test]
fn rail_lane_frame_time_over_2000_rail_ways() {
let Some((device, queue)) = common::device_or_skip("rail_dash_bench", "frame time over 2 000 rail ways") else {
return;
};
let pal = facett_map::MapPalette::cartographic(false);
let (mx0, my0) = facett_map::mercator(START.0, START.1);
let step = fixture_len() / 50.0;
let mut ways = Vec::with_capacity(2000);
for i in 0..2000i64 {
let row = (i / 50) as f64;
let col = (i % 50) as f64;
let pts: Vec<(f64, f64)> = (0..8)
.map(|k| {
let t = f64::from(k) / 7.0;
// One full sine period along the way (t runs 0..=1).
facett_map::unmercator(mx0 + (col + t) * step, my0 + row * step + (t * std::f64::consts::TAU).sin() * step * 0.1)
})
.collect();
let mut w = Way::from_lonlat_with(i, &pts, RAIL, &pal).expect("a rail way");
w.style.width = 2.0;
ways.push(w);
}
let origin = ways_origin(ways.iter()).expect("origin");
let geo = build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
let verts = geo.line_vertices;
let extent = fixture_len() as f32 * (1.0 + 40.0 / 50.0);
let t = MapTransform {
zoom_x: W as f32 / extent,
zoom_y: W as f32 / extent,
ref_x: (mx0 - origin[0]) as f32 + extent * 0.5,
ref_y: (my0 - origin[1]) as f32 + extent * 0.5,
screen_center_x: W as f32 * 0.5,
screen_center_y: H as f32 * 0.5,
bearing_deg: 0.0,
tilt_deg: 0.0,
};
use wgpu::util::DeviceExt;
let format = wgpu::TextureFormat::Rgba8Unorm;
let renderer = LineRenderer::new(&device, format);
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("bench_verts"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
renderer.write_uniforms(&queue, &t, W as f32, H as f32, 3.0, CASING, FEATHER_PX, 3.0);
// Each target names its sample count at a `sample_count:` field — the form
// `facett-core/tests/one_msaa_writer.rs` reads. The old helper closure took the count
// as a parameter (`sample_count: samples`), which the law cannot tell from a pinned
// literal (red on main since 5011265). The single-sample default is the ONE writer's
// constant; the two map targets override it with the map's sanctioned 4x constant.
let desc = |label, fmt| wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: facett_core::render::gpu::NO_MSAA_SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: fmt,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
};
let view = |d: wgpu::TextureDescriptor| device.create_texture(&d).create_view(&wgpu::TextureViewDescriptor::default());
let resolve = view(desc("bench_resolve", format));
let msaa = view(wgpu::TextureDescriptor {
sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
..desc("bench_msaa", format)
});
let depth = view(wgpu::TextureDescriptor {
sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
..desc("bench_depth", facett_map::gpu::MAP_DEPTH_FORMAT)
});
let mut samples_us = Vec::with_capacity(45);
for _ in 0..45 {
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("bench_enc") });
{
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("bench_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &msaa,
depth_slice: None,
resolve_target: Some(&resolve),
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
renderer.draw(&mut pass, &vbuf, verts.len() as u32);
}
let t0 = std::time::Instant::now();
queue.submit([enc.finish()]);
device.poll(wgpu::PollType::wait_indefinitely()).ok();
samples_us.push(t0.elapsed().as_micros() as u64);
}
let mut steady: Vec<u64> = samples_us[5..].to_vec();
steady.sort_unstable();
let median = steady[steady.len() / 2];
let min = steady[0];
let bytes = verts.len() * std::mem::size_of::<LineVertex>();
eprintln!(
"[rail-dash bench] 2000 rail ways, {} LineVertex ({bytes} B VRAM, {} B/vertex): frame median {median} µs, min {min} µs (wall, submit→fence, casing+fill, 512×512 4×MSAA)",
verts.len(),
std::mem::size_of::<LineVertex>()
);
assert!(median > 0, "a frame that takes no time was not rendered");
}