1use std::sync::Arc;
2use std::time::Instant;
3use std::{collections::VecDeque, env};
4
5use anyhow::{Context, Result};
6use bytemuck::{Pod, Zeroable};
7use wgpu::util::DeviceExt;
8use winit::{
9 dpi::PhysicalSize,
10 event::{Event, WindowEvent},
11 event_loop::EventLoop,
12 window::WindowBuilder,
13};
14
15use video_sys::VideoStream;
16
17#[repr(C)]
18#[derive(Clone, Copy, Debug, Pod, Zeroable)]
19struct Vertex {
20 pos: [f32; 2],
21 uv: [f32; 2],
22}
23
24static VERTEX_ATTRS: [wgpu::VertexAttribute; 2] =
25 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
26
27impl Vertex {
28 fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
29 wgpu::VertexBufferLayout {
30 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
31 step_mode: wgpu::VertexStepMode::Vertex,
32 attributes: &VERTEX_ATTRS,
33 }
34 }
35}
36
37struct State {
38 surface: wgpu::Surface<'static>,
39 device: wgpu::Device,
40 queue: wgpu::Queue,
41 config: wgpu::SurfaceConfiguration,
42 size: PhysicalSize<u32>,
43
44 pipeline: wgpu::RenderPipeline,
45 vbuf: wgpu::Buffer,
46 ibuf: wgpu::Buffer,
47 icount: u32,
48
49 texture: wgpu::Texture,
50 texture_view: wgpu::TextureView,
51 sampler: wgpu::Sampler,
52 bind_group: wgpu::BindGroup,
53
54 video: VideoStream,
55
56 stash: std::collections::VecDeque<video_sys::VideoFrame>,
57 started_at: Option<std::time::Instant>,
58 base_pts_us: Option<i64>,
59 last_presented_pts: i64,
60
61 render_fps_t0: Instant,
62 render_fps_n: u32,
63
64 video_fps_t0: Instant,
65 video_fps_n: u32,
66 video_delta_sum_us: i64,
67 video_delta_n: u32,
68}
69
70impl State {
71 async fn new(window: Arc<winit::window::Window>, path: &str) -> Result<Self> {
72 let size = window.inner_size();
73
74 let instance = wgpu::Instance::default();
75 let surface = instance
76 .create_surface(window.clone())
77 .context("create_surface")?;
78
79 let adapter = instance
80 .request_adapter(&wgpu::RequestAdapterOptions {
81 power_preference: wgpu::PowerPreference::HighPerformance,
82 compatible_surface: Some(&surface),
83 force_fallback_adapter: false,
84 })
85 .await
86 .context("request_adapter")?;
87
88 let (device, queue) = adapter
89 .request_device(
90 &wgpu::DeviceDescriptor {
91 label: None,
92 required_features: wgpu::Features::empty(),
93 required_limits: wgpu::Limits::default(),
94 },
95 None,
96 )
97 .await
98 .context("request_device")?;
99
100 let caps = surface.get_capabilities(&adapter);
101 let format = caps.formats[0];
102
103 let config = wgpu::SurfaceConfiguration {
104 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
105 format,
106 width: size.width.max(1),
107 height: size.height.max(1),
108 present_mode: wgpu::PresentMode::Fifo,
109 alpha_mode: caps.alpha_modes[0],
110 view_formats: vec![],
111 desired_maximum_frame_latency: 2,
112 };
113 surface.configure(&device, &config);
114
115 let video = VideoStream::open(path).context("VideoStream::open")?;
117 let tex_w = video.width();
118 let tex_h = video.height();
119
120 let texture = device.create_texture(&wgpu::TextureDescriptor {
122 label: Some("video_texture"),
123 size: wgpu::Extent3d {
124 width: tex_w,
125 height: tex_h,
126 depth_or_array_layers: 1,
127 },
128 mip_level_count: 1,
129 sample_count: 1,
130 dimension: wgpu::TextureDimension::D2,
131 format: wgpu::TextureFormat::Rgba8UnormSrgb,
132 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
133 view_formats: &[],
134 });
135 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
136 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
137 mag_filter: wgpu::FilterMode::Linear,
138 min_filter: wgpu::FilterMode::Linear,
139 ..Default::default()
140 });
141
142 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
144 label: Some("shader"),
145 source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
146 });
147
148 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
149 label: Some("bgl"),
150 entries: &[
151 wgpu::BindGroupLayoutEntry {
152 binding: 0,
153 visibility: wgpu::ShaderStages::FRAGMENT,
154 ty: wgpu::BindingType::Texture {
155 multisampled: false,
156 view_dimension: wgpu::TextureViewDimension::D2,
157 sample_type: wgpu::TextureSampleType::Float { filterable: true },
158 },
159 count: None,
160 },
161 wgpu::BindGroupLayoutEntry {
162 binding: 1,
163 visibility: wgpu::ShaderStages::FRAGMENT,
164 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
165 count: None,
166 },
167 ],
168 });
169
170 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
171 label: Some("bg"),
172 layout: &bind_group_layout,
173 entries: &[
174 wgpu::BindGroupEntry {
175 binding: 0,
176 resource: wgpu::BindingResource::TextureView(&texture_view),
177 },
178 wgpu::BindGroupEntry {
179 binding: 1,
180 resource: wgpu::BindingResource::Sampler(&sampler),
181 },
182 ],
183 });
184
185 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
186 label: Some("pl"),
187 bind_group_layouts: &[&bind_group_layout],
188 push_constant_ranges: &[],
189 });
190
191 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
192 label: Some("pipeline"),
193 layout: Some(&pipeline_layout),
194 vertex: wgpu::VertexState {
195 module: &shader,
196 entry_point: "vs_main",
197 buffers: &[Vertex::desc()],
198 },
199 fragment: Some(wgpu::FragmentState {
200 module: &shader,
201 entry_point: "fs_main",
202 targets: &[Some(wgpu::ColorTargetState {
203 format: config.format,
204 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
205 write_mask: wgpu::ColorWrites::ALL,
206 })],
207 }),
208 primitive: wgpu::PrimitiveState {
209 topology: wgpu::PrimitiveTopology::TriangleList,
210 ..Default::default()
211 },
212 depth_stencil: None,
213 multisample: wgpu::MultisampleState::default(),
214 multiview: None,
215 });
216
217 let vertices = [
218 Vertex {
219 pos: [-1.0, -1.0],
220 uv: [0.0, 1.0],
221 },
222 Vertex {
223 pos: [1.0, -1.0],
224 uv: [1.0, 1.0],
225 },
226 Vertex {
227 pos: [1.0, 1.0],
228 uv: [1.0, 0.0],
229 },
230 Vertex {
231 pos: [-1.0, 1.0],
232 uv: [0.0, 0.0],
233 },
234 ];
235 let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
236
237 let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
238 label: Some("vbuf"),
239 contents: bytemuck::cast_slice(&vertices),
240 usage: wgpu::BufferUsages::VERTEX,
241 });
242 let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
243 label: Some("ibuf"),
244 contents: bytemuck::cast_slice(&indices),
245 usage: wgpu::BufferUsages::INDEX,
246 });
247
248 Ok(Self {
249 surface,
250 device,
251 queue,
252 config,
253 size,
254 pipeline,
255 vbuf,
256 ibuf,
257 icount: indices.len() as u32,
258 texture,
259 texture_view,
260 sampler,
261 bind_group,
262 video,
263 stash: VecDeque::new(),
264 started_at: None,
265 base_pts_us: None,
266 last_presented_pts: -1,
267 render_fps_t0: Instant::now(),
268 render_fps_n: 0,
269 video_fps_t0: Instant::now(),
270 video_fps_n: 0,
271 video_delta_sum_us: 0,
272 video_delta_n: 0,
273 })
274 }
275
276 fn resize(&mut self, new_size: PhysicalSize<u32>) {
277 if new_size.width == 0 || new_size.height == 0 {
278 return;
279 }
280 self.size = new_size;
281 self.config.width = new_size.width;
282 self.config.height = new_size.height;
283 self.surface.configure(&self.device, &self.config);
284 }
285
286 fn update(&mut self) -> Result<()> {
287 while let Some(f) = self.video.try_recv_one() {
289 self.stash.push_back(f);
290 }
291
292 if self.started_at.is_none() {
294 if let Some(front) = self.stash.front() {
295 self.started_at = Some(std::time::Instant::now());
296 self.base_pts_us = Some(front.pts_us);
297 } else {
298 return Ok(());
299 }
300 }
301
302 let started_at = self.started_at.unwrap();
303 let elapsed_us = started_at.elapsed().as_micros() as i64;
304 let base = self.base_pts_us.unwrap();
305 let target_pts_us = base + elapsed_us;
306
307 let mut latest_due = None;
309 while let Some(front) = self.stash.front() {
310 if front.pts_us <= target_pts_us {
311 latest_due = self.stash.pop_front();
312 } else {
313 break;
314 }
315 }
316
317 if let Some(frame) = latest_due {
318 if frame.pts_us != self.last_presented_pts {
319 let dt = frame.pts_us - self.last_presented_pts;
320 log::info!("present dt={}us", dt);
321 self.last_presented_pts = frame.pts_us;
322 self.upload_frame(&frame.data, frame.width, frame.height);
323 }
324 }
325
326 Ok(())
327 }
328
329 fn upload_frame(&mut self, rgba: &[u8], width: u32, height: u32) {
330 let bytes_per_pixel = 4usize;
332 let row_bytes = width as usize * bytes_per_pixel;
333 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
334 let padded_row_bytes = (row_bytes + align - 1) / align * align;
335
336 if padded_row_bytes == row_bytes {
337 self.queue.write_texture(
338 wgpu::ImageCopyTexture {
339 texture: &self.texture,
340 mip_level: 0,
341 origin: wgpu::Origin3d::ZERO,
342 aspect: wgpu::TextureAspect::All,
343 },
344 rgba,
345 wgpu::ImageDataLayout {
346 offset: 0,
347 bytes_per_row: Some(row_bytes as u32),
348 rows_per_image: Some(height),
349 },
350 wgpu::Extent3d {
351 width,
352 height,
353 depth_or_array_layers: 1,
354 },
355 );
356 return;
357 }
358
359 let mut padded = vec![0u8; padded_row_bytes * height as usize];
360 for y in 0..height as usize {
361 let src = &rgba[y * row_bytes..(y + 1) * row_bytes];
362 let dst = &mut padded[y * padded_row_bytes..y * padded_row_bytes + row_bytes];
363 dst.copy_from_slice(src);
364 }
365
366 self.queue.write_texture(
367 wgpu::ImageCopyTexture {
368 texture: &self.texture,
369 mip_level: 0,
370 origin: wgpu::Origin3d::ZERO,
371 aspect: wgpu::TextureAspect::All,
372 },
373 &padded,
374 wgpu::ImageDataLayout {
375 offset: 0,
376 bytes_per_row: Some(padded_row_bytes as u32),
377 rows_per_image: Some(height),
378 },
379 wgpu::Extent3d {
380 width,
381 height,
382 depth_or_array_layers: 1,
383 },
384 );
385 }
386
387 fn render(&mut self) -> Result<()> {
388 let output = self
389 .surface
390 .get_current_texture()
391 .context("get_current_texture")?;
392 let view = output
393 .texture
394 .create_view(&wgpu::TextureViewDescriptor::default());
395
396 let mut encoder = self
397 .device
398 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
399 label: Some("encoder"),
400 });
401
402 {
403 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
404 label: Some("render_pass"),
405 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
406 view: &view,
407 resolve_target: None,
408 ops: wgpu::Operations {
409 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
410 store: wgpu::StoreOp::Store,
411 },
412 })],
413 depth_stencil_attachment: None,
414 timestamp_writes: None,
415 occlusion_query_set: None,
416 });
417
418 rpass.set_pipeline(&self.pipeline);
419 rpass.set_bind_group(0, &self.bind_group, &[]);
420 rpass.set_vertex_buffer(0, self.vbuf.slice(..));
421 rpass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint16);
422 rpass.draw_indexed(0..self.icount, 0, 0..1);
423 }
424
425 self.queue.submit(Some(encoder.finish()));
426 output.present();
427 Ok(())
428 }
429}
430
431fn main() -> Result<()> {
432 env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
433
434 let mut args = env::args().skip(1);
435 let path = args.next().context("Usage: video-player-wgpu <file.mp4>")?;
436
437 let event_loop = EventLoop::new()?;
438 let window = Arc::new(
439 WindowBuilder::new()
440 .with_title("video-player-wgpu")
441 .build(&event_loop)?,
442 );
443
444 let mut state = pollster::block_on(State::new(window.clone(), &path))?;
445
446 event_loop.run(move |event, elwt| {
447 elwt.set_control_flow(winit::event_loop::ControlFlow::Poll);
448
449 match event {
450 Event::WindowEvent { event, .. } => match event {
451 WindowEvent::CloseRequested => elwt.exit(),
452 WindowEvent::Resized(s) => state.resize(s),
453 WindowEvent::RedrawRequested => {
455 if let Err(e) = state.update().and_then(|_| state.render()) {
456 log::error!("render error: {e:?}");
457 }
458 }
459 _ => {}
460 },
461 Event::AboutToWait => {
462 window.request_redraw();
463 }
464 _ => {}
465 }
466 })?;
467
468 Ok(())
469}