1use crate::gfx::Gpu;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
9pub enum Filter {
10 Nearest,
12 #[default]
15 Sharp,
16 Xbr,
18 Fsr,
20 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#[derive(Clone, Copy, Debug, Default)]
41pub struct Present {
42 pub filter: Filter,
43 pub integer_scale: bool,
45}
46
47pub 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 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
71fn 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
344pub struct VideoFrame<'a> {
346 pub rgba: &'a [u8],
347 pub width: u32,
348 pub height: u32,
349 pub aspect: Option<(u32, u32)>,
351}
352
353struct Source {
354 texture: wgpu::Texture,
355 bind_group: wgpu::BindGroup,
357 bind_group_first: wgpu::BindGroup,
358 size: (u32, u32),
359}
360
361struct Mid {
363 texture: wgpu::Texture,
364 bind_group: wgpu::BindGroup,
365 size: (u32, u32),
366}
367
368pub 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 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 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 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 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
545fn 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 assert_eq!(letterbox((1280.0, 720.0), (320.0, 200.0), true, None), (160.0, 60.0, 3.0, 3.0));
568 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 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 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}