Skip to main content

gasm_host/
present.rs

1//! Showing `video_present` frames: the letterbox and the upscaling filters
2//! (design/presentation.md). Display only: hashes are taken from the guest's
3//! bytes before any of this runs.
4
5use crate::gfx::Gpu;
6
7/// How a 2D frame is scaled to the output.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
9pub enum Filter {
10    /// square pixels, nearest neighbour (linear when shrinking)
11    Nearest,
12    /// integer prescale + bilinear: even pixels at any factor, exactly nearest
13    /// at whole factors (the default)
14    #[default]
15    Sharp,
16    /// edge-directed (xBR level 2): rounds diagonals of pixel art
17    Xbr,
18    /// AMD FSR 1 (EASU upscale + RCAS sharpen): rendered and dithered content
19    Fsr,
20    /// a CRT look: scanlines (a beam that widens with brightness) and an aperture grille
21    Crt,
22}
23
24impl Filter {
25    pub const NAMES: &str = "nearest, sharp, xbr, fsr, crt";
26
27    pub fn parse(s: &str) -> Result<Filter, String> {
28        match s {
29            "nearest" => Ok(Filter::Nearest),
30            "sharp" => Ok(Filter::Sharp),
31            "xbr" => Ok(Filter::Xbr),
32            "fsr" => Ok(Filter::Fsr),
33            "crt" => Ok(Filter::Crt),
34            _ => Err(format!("unknown filter {s:?} (use {})", Filter::NAMES)),
35        }
36    }
37}
38
39/// The user's display settings for 2D frames.
40#[derive(Clone, Copy, Debug, Default)]
41pub struct Present {
42    pub filter: Filter,
43    /// scale by whole multiples only (black border around), when the frame fits
44    pub integer_scale: bool,
45}
46
47/// Where a `fw×fh` frame lands in a `dw×dh` output: (left, top, scale x, scale y),
48/// centred. `integer_scale`: whole multiples only (of the height), when the frame
49/// fits; `aspect`: the frame's display aspect num:den (`video_set_aspect`), None
50/// for square pixels. The pointer's frame position undoes exactly this
51/// (`host::frame_position`); same arithmetic in runners/web/lib/input.js.
52pub fn letterbox(drawable: (f64, f64), frame: (f64, f64), integer_scale: bool, aspect: Option<(u32, u32)>) -> (f64, f64, f64, f64) {
53    let (dw, dh, fw, fh) = (drawable.0, drawable.1, frame.0, frame.1);
54    let Some((n, d)) = aspect.map(|(n, d)| (n as f64, d as f64)) else {
55        let mut scale = (dw / fw).min(dh / fh);
56        if integer_scale && scale >= 1.0 {
57            scale = scale.floor();
58        }
59        return ((dw - fw * scale) / 2.0, (dh - fh * scale) / 2.0, scale, scale);
60    };
61    // shown fh*n/d wide per fh high; whole numbers are multiplied before dividing,
62    // so e.g. 320x200 at 4:3 gives exact scales
63    let mut scale = (dw * d / (fh * n)).min(dh / fh);
64    if integer_scale && scale >= 1.0 {
65        scale = scale.floor();
66    }
67    let sx = scale * fh * n / (d * fw);
68    ((dw - fw * sx) / 2.0, (dh - fh * scale) / 2.0, sx, scale)
69}
70
71/// The filter that actually runs at a scale factor: shrinking is always linear
72/// (`sharp` degrades to bilinear), and xBR needs room to draw its edges. FSR
73/// upscales at any factor above 1; scanlines need two output rows per line.
74fn effective(filter: Filter, scale: f64) -> Filter {
75    match filter {
76        _ if scale < 1.0 => Filter::Sharp,
77        Filter::Xbr if scale < 1.5 => Filter::Sharp,
78        Filter::Fsr if scale <= 1.0 => Filter::Sharp,
79        Filter::Crt if scale < 2.0 => Filter::Sharp,
80        f => f,
81    }
82}
83
84const SHADER: &str = r#"
85// src: frame size; out, origin: the letterboxed viewport (whole pixels)
86struct U { src: vec2<f32>, out: vec2<f32>, origin: vec2<f32> };
87@group(0) @binding(0) var t: texture_2d<f32>;
88@group(0) @binding(1) var lin: sampler;
89@group(0) @binding(2) var<uniform> u: U;
90
91// one triangle covering the viewport
92struct VO { @builtin(position) pos: vec4<f32> };
93@vertex fn vs(@builtin(vertex_index) i: u32) -> VO {
94  let c = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
95  return VO(vec4<f32>(c.x * 2.0 - 1.0, 1.0 - c.y * 2.0, 0.0, 1.0));
96}
97
98// The fragment's position in frame pixels, from its exact framebuffer position
99// (an interpolated uv rounds differently on different GPUs, and at some scales
100// pixel centres fall exactly on texel edges). The bias sends such ties the same
101// way everywhere; gasm-present.js does the same.
102fn frame_pos(i: VO) -> vec2<f32> {
103  return (i.pos.xy - u.origin) * u.src / u.out + 1.0 / 1024.0;
104}
105
106@fragment fn fs_nearest(i: VO) -> @location(0) vec4<f32> {
107  let size = vec2<i32>(textureDimensions(t));
108  return textureLoad(t, clamp(vec2<i32>(floor(frame_pos(i))), vec2<i32>(0), size - 1), 0);
109}
110
111// Sharp bilinear: as if the frame were first scaled by the largest whole factor
112// with nearest neighbour, then bilinearly to the output. Each pixel stays flat
113// and only its border, about one output pixel wide, blends with the next one.
114@fragment fn fs_sharp(i: VO) -> @location(0) vec4<f32> {
115  let texel = frame_pos(i);
116  let scale = max(floor(u.out / u.src), vec2<f32>(1.0));
117  let f = fract(texel) - 0.5;
118  let flat_half = 0.5 - 0.5 / scale + 1.0 / 512.0; // + the bias: whole factors give exactly nearest
119  let g = (f - clamp(f, -flat_half, flat_half)) * scale + 0.5;
120  return textureSampleLevel(t, lin, (floor(texel) + g) / u.src, 0.0);
121}
122
123// xBR level 2 (the algorithm of Hyllian's xBR-lv2 shaders, MIT; this is an
124// independent implementation). Per output pixel, look at the 21 source pixels
125// around it, find edges through each of the four corners of the centre pixel
126// E (at 30, 45 and 60 degrees) and blend E with the neighbour across the edge.
127// Any scale in one pass; the edge is antialiased over one output pixel.
128// Vectors hold the four corners (bottom-right, top-right, top-left,
129// bottom-left), each the previous one rotated by 90 degrees.
130const Y = vec3<f32>(14.352, 28.176, 5.472); // luma weights (x48)
131const EQ_THRESHOLD = 15.0;
132const LV2_COEFFICIENT = 2.0;
133
134fn px(c: vec2<i32>, dx: i32, dy: i32) -> vec3<f32> {
135  let size = vec2<i32>(textureDimensions(t));
136  return textureLoad(t, clamp(c + vec2<i32>(dx, dy), vec2<i32>(0), size - 1), 0).rgb;
137}
138fn df(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { return abs(a - b); }
139fn eq(a: vec4<f32>, b: vec4<f32>) -> vec4<bool> { return df(a, b) < vec4<f32>(EQ_THRESHOLD); }
140fn wd(a: vec4<f32>, b: vec4<f32>, c: vec4<f32>, d: vec4<f32>, e: vec4<f32>, f: vec4<f32>, g: vec4<f32>, h: vec4<f32>) -> vec4<f32> {
141  return df(a, b) + df(a, c) + df(d, e) + df(d, f) + 4.0 * df(g, h);
142}
143fn on(b: vec4<bool>) -> vec4<f32> { return select(vec4<f32>(0.0), vec4<f32>(1.0), b); }
144
145@fragment fn fs_xbr(i: VO) -> @location(0) vec4<f32> {
146  let pos = frame_pos(i);
147  let tc = floor(pos);
148  let fp = pos - tc;
149  let c0 = vec2<i32>(tc);
150  //     A1 B1 C1
151  //  A0 A  B  C  C4
152  //  D0 D  E  F  F4
153  //  G0 G  H  I  I4
154  //     G5 H5 I5
155  let A1 = px(c0, -1, -2); let B1 = px(c0, 0, -2); let C1 = px(c0, 1, -2);
156  let A0 = px(c0, -2, -1); let A = px(c0, -1, -1); let B = px(c0, 0, -1); let C = px(c0, 1, -1); let C4 = px(c0, 2, -1);
157  let D0 = px(c0, -2, 0); let D = px(c0, -1, 0); let E = px(c0, 0, 0); let F = px(c0, 1, 0); let F4 = px(c0, 2, 0);
158  let G0 = px(c0, -2, 1); let G = px(c0, -1, 1); let H = px(c0, 0, 1); let I = px(c0, 1, 1); let I4 = px(c0, 2, 1);
159  let G5 = px(c0, -1, 2); let H5 = px(c0, 0, 2); let I5 = px(c0, 1, 2);
160
161  let b = vec4<f32>(dot(B, Y), dot(D, Y), dot(H, Y), dot(F, Y));
162  let c = vec4<f32>(dot(C, Y), dot(A, Y), dot(G, Y), dot(I, Y));
163  let e = vec4<f32>(dot(E, Y));
164  let d = b.yzwx; let f = b.wxyz; let g = c.zwxy; let h = b.zwxy; let ii = c.wxyz;
165  let i4 = vec4<f32>(dot(I4, Y), dot(C1, Y), dot(A0, Y), dot(G5, Y));
166  let i5 = vec4<f32>(dot(I5, Y), dot(C4, Y), dot(A1, Y), dot(G0, Y));
167  let h5 = vec4<f32>(dot(H5, Y), dot(F4, Y), dot(B1, Y), dot(D0, Y));
168  let f4 = h5.yzwx;
169
170  // position within E along each corner's 45 / 30 / 60 degree direction
171  let fx = vec4<f32>(1.0, -1.0, -1.0, 1.0) * fp.y + vec4<f32>(1.0, 1.0, -1.0, -1.0) * fp.x;
172  let fx_left = vec4<f32>(1.0, -1.0, -1.0, 1.0) * fp.y + vec4<f32>(0.5, 2.0, -0.5, -2.0) * fp.x;
173  let fx_up = vec4<f32>(1.0, -1.0, -1.0, 1.0) * fp.y + vec4<f32>(2.0, 0.5, -2.0, -0.5) * fp.x;
174  let co = vec4<f32>(1.5, 0.5, -0.5, 0.5);
175  let cx = vec4<f32>(1.0, 1.0, -0.5, 0.0);
176  let cy = vec4<f32>(2.0, 0.0, -1.0, 0.5);
177  // half the edge's blend width: one output pixel, in source pixels
178  let s = max(min(u.out.x / u.src.x, u.out.y / u.src.y), 1.0);
179  let delta = vec4<f32>(1.0 / s);
180  let delta_l = vec4<f32>(0.5, 1.0, 0.5, 1.0) / s;
181  let delta_u = delta_l.yxwz;
182
183  let lv0 = (e != f) & (e != h);
184  let lv1 = lv0 & ((!eq(f, b) & !eq(h, d)) | (eq(e, ii) & !eq(f, i4) & !eq(h, i5)) | eq(e, g) | eq(e, c));
185  let lv2_left = (e != g) & (d != g);
186  let lv2_up = (e != c) & (b != c);
187  let wd1 = wd(e, c, g, ii, h5, f4, h, f);
188  let wd2 = wd(h, d, i5, f, i4, b, e, ii);
189  let edri = (wd1 <= wd2) & lv0;
190  let edr = (wd1 < wd2) & lv1;
191  let edr_left = (LV2_COEFFICIENT * df(f, g) <= df(h, c)) & lv2_left & edr;
192  let edr_up = (df(f, g) >= LV2_COEFFICIENT * df(h, c)) & lv2_up & edr;
193
194  let fx45 = on(edr) * saturate((fx + delta - co) / (2.0 * delta));
195  let fx45i = on(edri) * saturate((fx + delta - co - 0.25) / (2.0 * delta));
196  let fx30 = on(edr_left) * saturate((fx_left + delta_l - cx) / (2.0 * delta_l));
197  let fx60 = on(edr_up) * saturate((fx_up + delta_u - cy) / (2.0 * delta_u));
198  let m = max(max(fx30, fx60), max(fx45, fx45i));
199  // blend towards whichever of the two edge neighbours is closer to E
200  let p = df(e, f) <= df(e, h);
201  var r1 = mix(E, select(H, F, p.x), m.x);
202  r1 = mix(r1, select(B, D, p.z), m.z);
203  var r2 = mix(E, select(F, B, p.y), m.y);
204  r2 = mix(r2, select(D, H, p.w), m.w);
205  let d1 = dot(abs(E - r1), vec3<f32>(1.0));
206  let d2 = dot(abs(E - r2), vec3<f32>(1.0));
207  return vec4<f32>(select(r1, r2, d2 >= d1), 1.0);
208}
209
210// AMD FidelityFX Super Resolution 1 (MIT; an independent implementation of its
211// two passes). EASU: 12 source pixels around the output pixel, the local edge
212// direction and length from their luma, then a Lanczos-2-like kernel stretched
213// along the edge, clamped to the 4 nearest pixels (no ringing). Renders into an
214// intermediate texture the size of the viewport; RCAS then sharpens that.
215fn luma(c: vec3<f32>) -> f32 { return c.b * 0.5 + (c.r * 0.5 + c.g); }
216
217// one bilinear quadrant's contribution to direction and length; w: its weight,
218// a/b/c/d/e: luma above, left of, at, right of and below the quadrant's pixel
219fn easu_set(w: f32, a: f32, b: f32, c: f32, d: f32, e: f32) -> vec3<f32> {
220  let dc = d - c; let cb = c - b;
221  var lx = max(abs(dc), abs(cb));
222  lx = select(1.0 / lx, 0.0, lx == 0.0);
223  let dir_x = d - b;
224  lx = saturate(abs(dir_x) * lx); lx = lx * lx;
225  let ec = e - c; let ca = c - a;
226  var ly = max(abs(ec), abs(ca));
227  ly = select(1.0 / ly, 0.0, ly == 0.0);
228  let dir_y = e - a;
229  ly = saturate(abs(dir_y) * ly); ly = ly * ly;
230  return vec3<f32>(dir_x * w, dir_y * w, (lx + ly) * w);
231}
232
233fn easu_tap(off: vec2<f32>, dir: vec2<f32>, len2: vec2<f32>, lob: f32, clp: f32, c: vec3<f32>) -> vec4<f32> {
234  var v = vec2<f32>(off.x * dir.x + off.y * dir.y, off.x * -dir.y + off.y * dir.x);
235  v = v * len2;
236  let d2 = min(v.x * v.x + v.y * v.y, clp);
237  var wb = 0.4 * d2 - 1.0;
238  var wa = lob * d2 - 1.0;
239  wb = wb * wb; wa = wa * wa;
240  wb = 1.5625 * wb - 0.5625;
241  let w = wb * wa;
242  return vec4<f32>(c * w, w);
243}
244
245@fragment fn fs_easu(i: VO) -> @location(0) vec4<f32> {
246  // source position with pixel centres at whole numbers
247  let pp0 = frame_pos(i) - 0.5;
248  let fp = floor(pp0);
249  let pp = pp0 - fp;
250  let c0 = vec2<i32>(fp);
251  //    b c
252  //  e f g h
253  //  i j k l
254  //    n o
255  let b = px(c0, 0, -1); let c = px(c0, 1, -1);
256  let e = px(c0, -1, 0); let f = px(c0, 0, 0); let g = px(c0, 1, 0); let h = px(c0, 2, 0);
257  let ii = px(c0, -1, 1); let j = px(c0, 0, 1); let k = px(c0, 1, 1); let l = px(c0, 2, 1);
258  let n = px(c0, 0, 2); let o = px(c0, 1, 2);
259  let bl = luma(b); let cl = luma(c); let el = luma(e); let fl = luma(f); let gl = luma(g); let hl = luma(h);
260  let il = luma(ii); let jl = luma(j); let kl = luma(k); let ll = luma(l); let nl = luma(n); let ol = luma(o);
261  var acc = easu_set((1.0 - pp.x) * (1.0 - pp.y), bl, el, fl, gl, jl);
262  acc = acc + easu_set(pp.x * (1.0 - pp.y), cl, fl, gl, hl, kl);
263  acc = acc + easu_set((1.0 - pp.x) * pp.y, fl, il, jl, kl, nl);
264  acc = acc + easu_set(pp.x * pp.y, gl, jl, kl, ll, ol);
265  var dir = acc.xy;
266  var len = acc.z;
267  let dr = dir.x * dir.x + dir.y * dir.y;
268  let zero = dr < 1.0 / 32768.0;
269  dir = select(dir * inverseSqrt(dr), vec2<f32>(1.0, 0.0), zero);
270  len = len * 0.5; len = len * len;
271  let stretch = (dir.x * dir.x + dir.y * dir.y) / max(abs(dir.x), abs(dir.y));
272  let len2 = vec2<f32>(1.0 + (stretch - 1.0) * len, 1.0 - 0.5 * len);
273  let lob = 0.5 + ((1.0 / 4.0 - 0.04) - 0.5) * len;
274  let clp = 1.0 / lob;
275  var a = vec4<f32>(0.0);
276  a = a + easu_tap(vec2<f32>(0.0, -1.0) - pp, dir, len2, lob, clp, b);
277  a = a + easu_tap(vec2<f32>(1.0, -1.0) - pp, dir, len2, lob, clp, c);
278  a = a + easu_tap(vec2<f32>(-1.0, 1.0) - pp, dir, len2, lob, clp, ii);
279  a = a + easu_tap(vec2<f32>(0.0, 1.0) - pp, dir, len2, lob, clp, j);
280  a = a + easu_tap(vec2<f32>(0.0, 0.0) - pp, dir, len2, lob, clp, f);
281  a = a + easu_tap(vec2<f32>(-1.0, 0.0) - pp, dir, len2, lob, clp, e);
282  a = a + easu_tap(vec2<f32>(1.0, 1.0) - pp, dir, len2, lob, clp, k);
283  a = a + easu_tap(vec2<f32>(2.0, 1.0) - pp, dir, len2, lob, clp, l);
284  a = a + easu_tap(vec2<f32>(2.0, 0.0) - pp, dir, len2, lob, clp, h);
285  a = a + easu_tap(vec2<f32>(1.0, 0.0) - pp, dir, len2, lob, clp, g);
286  a = a + easu_tap(vec2<f32>(1.0, 2.0) - pp, dir, len2, lob, clp, o);
287  a = a + easu_tap(vec2<f32>(0.0, 2.0) - pp, dir, len2, lob, clp, n);
288  let mn = min(min(f, g), min(j, k));
289  let mx = max(max(f, g), max(j, k));
290  return vec4<f32>(min(mx, max(mn, a.rgb / a.w)), 1.0);
291}
292
293// CRT (own design): each output pixel sees the two nearest source lines as
294// beams of light whose height grows with their brightness, horizontally as
295// sharp bilinear; lit in linear light, then an aperture grille (RGB stripes,
296// one output pixel each) and a boost for the light the dark gaps take away.
297const CRT_MASK = 0.3;   // how much a stripe dims the other two colours
298const CRT_BOOST = 1.45;
299fn crt_line(x: f32, line: f32) -> vec3<f32> {
300  let c = textureSampleLevel(t, lin, vec2<f32>(x, line + 0.5) / u.src, 0.0).rgb;
301  return pow(c, vec3<f32>(2.4));
302}
303fn crt_beam(d: f32, c: vec3<f32>) -> vec3<f32> {
304  let width = mix(vec3<f32>(0.3), vec3<f32>(0.55), sqrt(c));
305  let e = d / width;
306  return exp(-e * e);
307}
308@fragment fn fs_crt(i: VO) -> @location(0) vec4<f32> {
309  let pos = frame_pos(i);
310  let sx = max(floor(u.out.x / u.src.x), 1.0);
311  let f = fract(pos.x) - 0.5;
312  let flat_half = 0.5 - 0.5 / sx + 1.0 / 512.0;
313  let x = floor(pos.x) + (f - clamp(f, -flat_half, flat_half)) * sx + 0.5;
314  let y = pos.y - 0.5;
315  let line = floor(y);
316  let d = y - line;
317  let c0 = crt_line(x, line);
318  let c1 = crt_line(x, min(line + 1.0, u.src.y - 1.0));
319  var col = c0 * crt_beam(d, c0) + c1 * crt_beam(1.0 - d, c1);
320  let stripe = u32(floor(i.pos.x)) % 3u;
321  let mask = select(vec3<f32>(1.0 - CRT_MASK), vec3<f32>(1.0), vec3<u32>(0u, 1u, 2u) == vec3<u32>(stripe));
322  col = col * mask * CRT_BOOST;
323  return vec4<f32>(pow(min(col, vec3<f32>(1.0)), vec3<f32>(1.0 / 2.4)), 1.0);
324}
325
326// RCAS: sharpen the EASU output (t is now that intermediate texture, one texel
327// per viewport pixel) with a 5-tap cross, as strong as the neighbourhood allows
328// without clipping. 0.2 stops below maximum sharpness, FSR's default.
329const RCAS_SHARPNESS = 0.87055056; // exp2(-0.2)
330const RCAS_LIMIT = 0.1875;          // 0.25 - 1/16
331@fragment fn fs_rcas(i: VO) -> @location(0) vec4<f32> {
332  let c0 = vec2<i32>(floor(i.pos.xy - u.origin));
333  let b = px(c0, 0, -1); let d = px(c0, -1, 0); let e = px(c0, 0, 0); let f = px(c0, 1, 0); let h = px(c0, 0, 1);
334  let mn = min(min(b, d), min(f, h));
335  let mx = max(max(b, d), max(f, h));
336  let hit_min = mn / max(4.0 * mx, vec3<f32>(1.0 / 1024.0));
337  let hit_max = (1.0 - mx) / min(4.0 * mn - 4.0, vec3<f32>(-1.0 / 1024.0));
338  let lobe_rgb = max(-hit_min, hit_max);
339  let lobe = max(-RCAS_LIMIT, min(max(lobe_rgb.r, max(lobe_rgb.g, lobe_rgb.b)), 0.0)) * RCAS_SHARPNESS;
340  return vec4<f32>((lobe * (b + d + f + h) + e) / (4.0 * lobe + 1.0), 1.0);
341}
342"#;
343
344/// A `video_present` frame to show: tightly packed RGBA rows and its display aspect.
345pub struct VideoFrame<'a> {
346    pub rgba: &'a [u8],
347    pub width: u32,
348    pub height: u32,
349    /// `video_set_aspect` (num, den); None: square pixels
350    pub aspect: Option<(u32, u32)>,
351}
352
353struct Source {
354    texture: wgpu::Texture,
355    /// with `uniform` (drawn into the output) and with `first` (drawn into `mid`)
356    bind_group: wgpu::BindGroup,
357    bind_group_first: wgpu::BindGroup,
358    size: (u32, u32),
359}
360
361/// The intermediate image of two-pass filters (FSR), one texel per viewport pixel.
362struct Mid {
363    texture: wgpu::Texture,
364    bind_group: wgpu::BindGroup,
365    size: (u32, u32),
366}
367
368/// GPU side of showing 2D frames: one pipeline per filter pass, the frame
369/// texture, the intermediate image of two-pass filters and two uniforms with the
370/// sizes (for the pass into the output, and for a first pass into `mid`).
371pub struct Presenter {
372    nearest: wgpu::RenderPipeline,
373    sharp: wgpu::RenderPipeline,
374    xbr: wgpu::RenderPipeline,
375    easu: wgpu::RenderPipeline,
376    rcas: wgpu::RenderPipeline,
377    crt: wgpu::RenderPipeline,
378    layout: wgpu::BindGroupLayout,
379    linear: wgpu::Sampler,
380    uniform: wgpu::Buffer,
381    first: wgpu::Buffer,
382    source: Option<Source>,
383    mid: Option<Mid>,
384}
385
386const MID_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
387
388impl Presenter {
389    pub fn new(gpu: &Gpu) -> Presenter {
390        let dev = &gpu.device;
391        let module = dev.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("present"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
392        let frag = wgpu::ShaderStages::FRAGMENT;
393        let entry = |binding, ty| wgpu::BindGroupLayoutEntry { binding, visibility: frag, ty, count: None };
394        let layout = dev.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
395            label: Some("present"),
396            entries: &[
397                entry(0, wgpu::BindingType::Texture {
398                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
399                    view_dimension: wgpu::TextureViewDimension::D2,
400                    multisampled: false,
401                }),
402                entry(1, wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering)),
403                entry(2, wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: wgpu::BufferSize::new(24) }),
404            ],
405        });
406        let pl = dev.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("present"), bind_group_layouts: &[Some(&layout)], immediate_size: 0 });
407        let pipeline = |fs: &str, format: wgpu::TextureFormat| {
408            dev.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
409                label: Some(fs),
410                layout: Some(&pl),
411                vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), compilation_options: Default::default(), buffers: &[] },
412                primitive: Default::default(),
413                depth_stencil: None,
414                multisample: Default::default(),
415                fragment: Some(wgpu::FragmentState { module: &module, entry_point: Some(fs), compilation_options: Default::default(), targets: &[Some(format.into())] }),
416                multiview_mask: None,
417                cache: None,
418            })
419        };
420        let uniform = || {
421            dev.create_buffer(&wgpu::BufferDescriptor {
422                label: Some("present"),
423                size: 32,
424                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
425                mapped_at_creation: false,
426            })
427        };
428        Presenter {
429            nearest: pipeline("fs_nearest", gpu.format),
430            sharp: pipeline("fs_sharp", gpu.format),
431            xbr: pipeline("fs_xbr", gpu.format),
432            easu: pipeline("fs_easu", MID_FORMAT),
433            rcas: pipeline("fs_rcas", gpu.format),
434            crt: pipeline("fs_crt", gpu.format),
435            linear: dev.create_sampler(&wgpu::SamplerDescriptor { mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() }),
436            uniform: uniform(),
437            first: uniform(),
438            layout,
439            source: None,
440            mid: None,
441        }
442    }
443
444    fn bind_group(&self, gpu: &Gpu, view: &wgpu::TextureView, uniform: &wgpu::Buffer) -> wgpu::BindGroup {
445        gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
446            label: Some("present"),
447            layout: &self.layout,
448            entries: &[
449                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(view) },
450                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.linear) },
451                wgpu::BindGroupEntry { binding: 2, resource: uniform.as_entire_binding() },
452            ],
453        })
454    }
455
456    /// Upload the frame (a new texture only when its size changes).
457    fn upload(&mut self, gpu: &Gpu, rgba: &[u8], w: u32, h: u32) {
458        let extent = wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 };
459        if !matches!(&self.source, Some(s) if s.size == (w, h)) {
460            let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
461                label: Some("video"),
462                size: extent,
463                mip_level_count: 1,
464                sample_count: 1,
465                dimension: wgpu::TextureDimension::D2,
466                format: wgpu::TextureFormat::Rgba8Unorm,
467                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
468                view_formats: &[],
469            });
470            let view = texture.create_view(&Default::default());
471            let bind_group = self.bind_group(gpu, &view, &self.uniform);
472            let bind_group_first = self.bind_group(gpu, &view, &self.first);
473            self.source = Some(Source { texture, bind_group, bind_group_first, size: (w, h) });
474        }
475        let tex = &self.source.as_ref().unwrap().texture;
476        gpu.queue.write_texture(
477            tex.as_image_copy(),
478            rgba,
479            wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 4), rows_per_image: Some(h) },
480            extent,
481        );
482    }
483
484    /// Draw a `w×h` RGBA frame at display aspect `aspect` (None: square pixels)
485    /// letterboxed into `view`, an output of `size` pixels.
486    pub fn draw(&mut self, gpu: &Gpu, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView, size: (u32, u32), frame: VideoFrame, opts: Present) {
487        let VideoFrame { rgba, width: w, height: h, aspect } = frame;
488        self.upload(gpu, rgba, w, h);
489        let (ox, oy, sx, sy) = letterbox((size.0 as f64, size.1 as f64), (w as f64, h as f64), opts.integer_scale, aspect);
490        // whole pixels, as WebGL viewports are (gasm-present.js), so both runners show the same image
491        let (vx, vy, vw, vh) = (ox.round() as f32, oy.round() as f32, (w as f64 * sx).round() as f32, (h as f64 * sy).round() as f32);
492        let write = |buf: &wgpu::Buffer, origin: (f32, f32)| {
493            let u: [f32; 8] = [w as f32, h as f32, vw, vh, origin.0, origin.1, 0.0, 0.0];
494            gpu.queue.write_buffer(buf, 0, &u.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<_>>());
495        };
496        write(&self.uniform, (vx, vy));
497        let filter = effective(opts.filter, sx.min(sy));
498        if filter == Filter::Fsr {
499            // EASU into `mid` (viewport-sized, origin 0), then RCAS from it into the output
500            write(&self.first, (0.0, 0.0));
501            let size = (vw as u32, vh as u32);
502            if !matches!(&self.mid, Some(m) if m.size == size) {
503                let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
504                    label: Some("present mid"),
505                    size: wgpu::Extent3d { width: size.0, height: size.1, depth_or_array_layers: 1 },
506                    mip_level_count: 1,
507                    sample_count: 1,
508                    dimension: wgpu::TextureDimension::D2,
509                    format: MID_FORMAT,
510                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
511                    view_formats: &[],
512                });
513                let bind_group = self.bind_group(gpu, &texture.create_view(&Default::default()), &self.uniform);
514                self.mid = Some(Mid { texture, bind_group, size });
515            }
516            let mid = self.mid.as_ref().unwrap();
517            let mid_view = mid.texture.create_view(&Default::default());
518            let mut pass = begin(encoder, &mid_view);
519            pass.set_pipeline(&self.easu);
520            pass.set_bind_group(0, &self.source.as_ref().unwrap().bind_group_first, &[]);
521            pass.draw(0..3, 0..1);
522            drop(pass);
523            let mut pass = begin(encoder, view);
524            pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
525            pass.set_pipeline(&self.rcas);
526            pass.set_bind_group(0, &mid.bind_group, &[]);
527            pass.draw(0..3, 0..1);
528            return;
529        }
530        let pipeline = match filter {
531            Filter::Nearest => &self.nearest,
532            Filter::Sharp => &self.sharp,
533            Filter::Xbr => &self.xbr,
534            Filter::Crt => &self.crt,
535            Filter::Fsr => unreachable!(),
536        };
537        let mut pass = begin(encoder, view);
538        pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
539        pass.set_pipeline(pipeline);
540        pass.set_bind_group(0, &self.source.as_ref().unwrap().bind_group, &[]);
541        pass.draw(0..3, 0..1);
542    }
543}
544
545/// A render pass into `view`, cleared to black.
546fn begin<'a>(encoder: &'a mut wgpu::CommandEncoder, view: &'a wgpu::TextureView) -> wgpu::RenderPass<'a> {
547    encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
548        label: Some("present"),
549        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
550            view,
551            depth_slice: None,
552            resolve_target: None,
553            ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
554        })],
555        ..Default::default()
556    })
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn letterbox_matches_plain_scaling_by_default() {
565        assert_eq!(letterbox((1280.0, 720.0), (320.0, 200.0), false, None), (64.0, 0.0, 3.6, 3.6));
566        // integer: 3x, centred
567        assert_eq!(letterbox((1280.0, 720.0), (320.0, 200.0), true, None), (160.0, 60.0, 3.0, 3.0));
568        // a frame larger than the output still shrinks to fit
569        assert_eq!(letterbox((320.0, 200.0), (640.0, 400.0), true, None), (0.0, 0.0, 0.5, 0.5));
570    }
571
572    #[test]
573    fn letterbox_with_display_aspect() {
574        // 320x200 at 4:3 in 1280x720: shown 960x720 (the JS letterbox gives the same values)
575        let (ox, oy, sx, sy) = letterbox((1280.0, 720.0), (320.0, 200.0), false, Some((4, 3)));
576        assert_eq!((ox, oy, sx, sy), (160.0, 0.0, 3.0, 3.6));
577        // integer: 3x vertically, 800x600
578        assert_eq!(letterbox((1280.0, 720.0), (320.0, 200.0), true, Some((4, 3))), (240.0, 60.0, 2.5, 3.0));
579    }
580
581    #[test]
582    fn filters_fall_back_when_there_is_no_room() {
583        assert_eq!(effective(Filter::Xbr, 1.2), Filter::Sharp);
584        assert_eq!(effective(Filter::Xbr, 3.0), Filter::Xbr);
585        assert_eq!(effective(Filter::Nearest, 0.5), Filter::Sharp);
586        assert_eq!(effective(Filter::Nearest, 2.5), Filter::Nearest);
587        assert_eq!(effective(Filter::Fsr, 1.0), Filter::Sharp);
588        assert_eq!(effective(Filter::Fsr, 1.25), Filter::Fsr);
589        assert_eq!(effective(Filter::Crt, 1.9), Filter::Sharp);
590        assert_eq!(effective(Filter::Crt, 2.0), Filter::Crt);
591    }
592}