diff --git a/Graphics/src/shaders/mod.rs b/Graphics/src/shaders/mod.rs index f0986c5f5..5dd9ac173 100644 --- a/Graphics/src/shaders/mod.rs +++ b/Graphics/src/shaders/mod.rs @@ -9,28 +9,34 @@ use aabb::queue_aabb; use bytemuck::Pod; use bytemuck_derive::{Pod, Zeroable}; use cxx::{type_id, ExternType}; -use cxx::private::hash; use fog_volume_filter::queue_fog_volume_filter; use fog_volume_plane::queue_fog_volume_plane; use model::{add_material_set, add_model}; -use texture::{create_render_texture, create_static_texture_2d, drop_texture}; -use textured_quad::{queue_textured_quad_verts, queue_textured_quad}; +use movie_player::queue_movie_player; +use texture::{ + create_dynamic_texture_2d, create_render_texture, create_static_texture_2d, drop_texture, + write_texture, +}; +use textured_quad::{queue_textured_quad, queue_textured_quad_verts}; use twox_hash::Xxh3Hash64; use wgpu::RenderPipeline; use crate::{ gpu::GraphicsConfig, + shaders::{ + ffi::{TextureFormat, TextureRef}, + texture::{RenderTexture, TextureWithView}, + }, zeus::{CColor, CMatrix4f, CRectangle, CVector2f, CVector3f, IDENTITY_MATRIX4F}, }; -use crate::shaders::ffi::{TextureFormat, TextureRef}; -use crate::shaders::texture::{RenderTexture, TextureWithView}; mod aabb; mod fog_volume_filter; mod fog_volume_plane; mod model; -mod textured_quad; +mod movie_player; mod texture; +mod textured_quad; #[cxx::bridge] mod ffi { @@ -203,6 +209,14 @@ mod ffi { rect: CRectangle, z: f32, ); + fn queue_movie_player( + tex_y: TextureRef, + tex_u: TextureRef, + tex_v: TextureRef, + color: CColor, + h_pad: f32, + v_pad: f32, + ); fn create_static_texture_2d( width: u32, @@ -212,6 +226,13 @@ mod ffi { data: &[u8], label: &str, ) -> TextureRef; + fn create_dynamic_texture_2d( + width: u32, + height: u32, + mips: u32, + format: TextureFormat, + label: &str, + ) -> TextureRef; fn create_render_texture( width: u32, height: u32, @@ -220,6 +241,7 @@ mod ffi { depth_bind_count: u32, label: &str, ) -> TextureRef; + fn write_texture(handle: TextureRef, data: &[u8]); fn drop_texture(handle: TextureRef); } } @@ -310,7 +332,7 @@ enum ShaderDrawCommand { game_blend_mode: u32, model_flags: u32, }, - MoviePlayer {/* TODO */}, + MoviePlayer(movie_player::DrawData), NES {/* TODO */}, ParticleSwoosh {/* TODO */}, PhazonSuitFilter {/* TODO */}, @@ -347,6 +369,7 @@ struct RenderState { // Shader states aabb: aabb::State, textured_quad: textured_quad::State, + movie_player: movie_player::State, } pub(crate) fn construct_state( device: Arc, @@ -370,6 +393,7 @@ pub(crate) fn construct_state( }; let aabb = aabb::construct_state(&device, &queue, &buffers); let textured_quad = textured_quad::construct_state(&device, &queue, &buffers, graphics_config); + let movie_player = movie_player::construct_state(&device, &queue, &buffers, graphics_config); let mut state = RenderState { device: device.clone(), queue: queue.clone(), @@ -384,6 +408,7 @@ pub(crate) fn construct_state( render_textures: Default::default(), aabb, textured_quad, + movie_player, }; for config in aabb::INITIAL_PIPELINES { construct_pipeline(&mut state, config); @@ -486,6 +511,7 @@ struct PipelineRef { pub(crate) enum PipelineCreateCommand { Aabb(aabb::PipelineConfig), TexturedQuad(textured_quad::PipelineConfig), + MoviePlayer(movie_player::PipelineConfig), } #[inline(always)] fn hash_with_seed(value: &T, seed: u64) -> u64 { @@ -497,6 +523,7 @@ fn construct_pipeline(state: &mut RenderState, cmd: &PipelineCreateCommand) -> u let id = match cmd { PipelineCreateCommand::Aabb(ref config) => hash_with_seed(config, 0xAABB), PipelineCreateCommand::TexturedQuad(ref config) => hash_with_seed(config, 0xEEAD), + PipelineCreateCommand::MoviePlayer(ref config) => hash_with_seed(config, 0xFAAE), }; if !state.pipelines.contains_key(&id) { let pipeline = match cmd { @@ -512,6 +539,12 @@ fn construct_pipeline(state: &mut RenderState, cmd: &PipelineCreateCommand) -> u &state.textured_quad, config, ), + PipelineCreateCommand::MoviePlayer(ref config) => movie_player::construct_pipeline( + state.device.as_ref(), + &state.graphics_config, + &state.movie_player, + config, + ), }; state.pipelines.insert(id, pipeline); } @@ -566,7 +599,20 @@ pub(crate) fn render_into_pass(pass: &mut wgpu::RenderPass) { aabb::draw_aabb(data, &state.aabb, pass, &state.buffers); } ShaderDrawCommand::TexturedQuad(data) => { - textured_quad::draw_textured_quad(data, &state.textured_quad, pass, &state.buffers); + textured_quad::draw_textured_quad( + data, + &state.textured_quad, + pass, + &state.buffers, + ); + } + ShaderDrawCommand::MoviePlayer(data) => { + movie_player::draw_movie_player( + data, + &state.movie_player, + pass, + &state.buffers, + ); } _ => todo!(), }, diff --git a/Graphics/src/shaders/movie_player/mod.rs b/Graphics/src/shaders/movie_player/mod.rs new file mode 100644 index 000000000..56c8b7610 --- /dev/null +++ b/Graphics/src/shaders/movie_player/mod.rs @@ -0,0 +1,301 @@ +use std::{ + collections::HashMap, + hash::{Hash, Hasher}, + ops::Range, +}; + +use bytemuck_derive::{Pod, Zeroable}; +use wgpu::{include_wgsl, vertex_attr_array}; + +use crate::{ + get_app, + gpu::GraphicsConfig, + shaders::{ + bind_pipeline, + ffi::{CameraFilterType, TextureRef, ZTest}, + pipeline_ref, push_draw_command, push_uniform, push_verts, + texture::create_sampler, + BuiltBuffers, PipelineCreateCommand, PipelineHolder, PipelineRef, ShaderDrawCommand, STATE, + }, + util::{align, Vec2, Vec3}, + zeus::{CColor, CMatrix4f, CRectangle, CVector4f}, +}; + +#[derive(Debug, Clone)] +pub(crate) struct DrawData { + pipeline: PipelineRef, + vert_range: Range, + uniform_range: Range, + bind_group_id: u32, +} + +#[derive(Hash)] +pub(crate) struct PipelineConfig { + // nothing +} +pub(crate) const INITIAL_PIPELINES: &[PipelineCreateCommand] = + &[PipelineCreateCommand::MoviePlayer(PipelineConfig {})]; + +pub(crate) struct State { + shader: wgpu::ShaderModule, + uniform_layout: wgpu::BindGroupLayout, + uniform_bind_group: wgpu::BindGroup, + texture_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, + pipeline_layout: wgpu::PipelineLayout, + // Transient state + texture_bind_groups: HashMap, + frame_used_textures: Vec, // TODO use to clear bind groups +} + +pub(crate) fn construct_state( + device: &wgpu::Device, + _queue: &wgpu::Queue, + buffers: &BuiltBuffers, + graphics_config: &GraphicsConfig, +) -> State { + let shader = device.create_shader_module(&include_wgsl!("shader.wgsl")); + let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment; + let uniform_size = wgpu::BufferSize::new(align( + std::mem::size_of::() as u64, + uniform_alignment as u64, + )); + let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Textured Quad Uniform Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: uniform_size, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let sampler = create_sampler(device, wgpu::AddressMode::Repeat, None); + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Textured Quad Uniform Bind Group"), + layout: &uniform_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &buffers.uniform_buffer, + offset: 0, // dynamic + size: uniform_size, + }), + }, + wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) }, + ], + }); + let texture_binding = wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }; + let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Textured Quad Texture Bind Group Layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: texture_binding, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: texture_binding, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: texture_binding, + count: None, + }, + ], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Textured Quad Pipeline Layout"), + bind_group_layouts: &[&uniform_layout, &texture_layout], + push_constant_ranges: &[], + }); + State { + shader, + uniform_layout, + uniform_bind_group, + texture_layout, + sampler, + pipeline_layout, + texture_bind_groups: Default::default(), + frame_used_textures: vec![], + } +} + +pub(crate) fn construct_pipeline( + device: &wgpu::Device, + graphics: &GraphicsConfig, + state: &State, + config: &PipelineConfig, +) -> PipelineHolder { + PipelineHolder { + pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Textured Quad Pipeline"), + layout: Some(&state.pipeline_layout), + vertex: wgpu::VertexState { + module: &state.shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &vertex_attr_array![0 => Float32x3, 1 => Float32x2], + }], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: graphics.depth_format, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Always, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: wgpu::MultisampleState { + count: graphics.msaa_samples, + ..Default::default() + }, + fragment: Some(wgpu::FragmentState { + module: &state.shader, + entry_point: "fs_main", + targets: &[wgpu::ColorTargetState { + format: graphics.color_format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::COLOR, + }], + }), + multiview: None, + }), + } +} + +#[derive(Pod, Zeroable, Copy, Clone, Default, Debug)] +#[repr(C)] +struct Uniform { + xf: CMatrix4f, + color: CColor, +} + +#[derive(Pod, Zeroable, Copy, Clone, Default, Debug)] +#[repr(C)] +struct Vert { + pos: Vec3, + uv: Vec2, +} + +pub(crate) fn queue_movie_player( + tex_y: TextureRef, + tex_u: TextureRef, + tex_v: TextureRef, + color: CColor, + h_pad: f32, + v_pad: f32, +) { + let pipeline = pipeline_ref(&PipelineCreateCommand::MoviePlayer(PipelineConfig {})); + let vert_range = push_verts(&[ + Vert { pos: Vec3::new(-h_pad, v_pad, 0.0), uv: Vec2::new(0.0, 0.0) }, + Vert { pos: Vec3::new(-h_pad, -v_pad, 0.0), uv: Vec2::new(0.0, 1.0) }, + Vert { pos: Vec3::new(h_pad, v_pad, 0.0), uv: Vec2::new(1.0, 0.0) }, + Vert { pos: Vec3::new(h_pad, -v_pad, 0.0), uv: Vec2::new(1.0, 1.0) }, + ]); + let uniform_range = push_uniform(&Uniform { xf: CMatrix4f::default(), color }); + + // TODO defer bind group creation to draw time or another thread? + let state = unsafe { STATE.as_mut().unwrap_unchecked() }; + let groups = &mut state.movie_player.texture_bind_groups; + let mut hasher = twox_hash::XxHash32::default(); + tex_y.id.hash(&mut hasher); + tex_u.id.hash(&mut hasher); + tex_v.id.hash(&mut hasher); + let bind_group_id = hasher.finish() as u32; + if !groups.contains_key(&bind_group_id) { + let tex_y = state.textures.get(&tex_y.id).unwrap(); + let tex_u = state.textures.get(&tex_u.id).unwrap(); + let tex_v = state.textures.get(&tex_v.id).unwrap(); + let bind_group = get_app().gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &state.movie_player.texture_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&tex_y.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&tex_u.view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&tex_v.view), + }, + ], + }); + groups.insert(bind_group_id, bind_group); + } + + push_draw_command(ShaderDrawCommand::MoviePlayer(DrawData { + pipeline, + vert_range, + uniform_range, + bind_group_id, + })); +} + +pub(crate) fn draw_movie_player<'a>( + data: &DrawData, + state: &'a State, + pass: &mut wgpu::RenderPass<'a>, + buffers: &'a BuiltBuffers, +) { + if !bind_pipeline(data.pipeline, pass) { + return; + } + + // Uniform bind group + pass.set_bind_group(0, &state.uniform_bind_group, &[ + data.uniform_range.start as wgpu::DynamicOffset + ]); + + // Texture bind group + let texture_bind_group = state + .texture_bind_groups + .get(&data.bind_group_id) + .expect("Failed to find texture bind group"); + pass.set_bind_group(1, texture_bind_group, &[]); + + // Vertex buffer + pass.set_vertex_buffer(0, buffers.vertex_buffer.slice(data.vert_range.clone())); + pass.draw(0..4, 0..1); +} diff --git a/Graphics/src/shaders/movie_player/shader.wgsl b/Graphics/src/shaders/movie_player/shader.wgsl new file mode 100644 index 000000000..5428d36e5 --- /dev/null +++ b/Graphics/src/shaders/movie_player/shader.wgsl @@ -0,0 +1,42 @@ +struct Uniform { + xf: mat4x4; + color: vec4; +}; +@group(0) @binding(0) +var ubuf: Uniform; +@group(0) @binding(1) +var tex_sampler: sampler; +@group(1) @binding(0) +var tex_y: texture_2d; +@group(1) @binding(1) +var tex_u: texture_2d; +@group(1) @binding(2) +var tex_v: texture_2d; + +struct VertexOutput { + @builtin(position) pos: vec4; + @location(0) uv: vec2; +}; + +@stage(vertex) +fn vs_main(@location(0) in_pos: vec3, @location(1) in_uv: vec2) -> VertexOutput { + var out: VertexOutput; + out.pos = ubuf.xf * vec4(in_pos, 1.0); + out.uv = in_uv; + return out; +} + +@stage(fragment) +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + var yuv = vec3( + 1.1643 * (textureSample(tex_y, tex_sampler, in.uv).x - 0.0625), + textureSample(tex_u, tex_sampler, in.uv).x - 0.5, + textureSample(tex_v, tex_sampler, in.uv).x - 0.5 + ); + return ubuf.color * vec4( + yuv.x + 1.5958 * yuv.z, + yuv.x - 0.39173 * yuv.y - 0.8129 * yuv.z, + yuv.x + 2.017 * yuv.y, + 1.0 + ); +} diff --git a/Graphics/src/shaders/texture.rs b/Graphics/src/shaders/texture.rs index d83e69348..0bd7bf918 100644 --- a/Graphics/src/shaders/texture.rs +++ b/Graphics/src/shaders/texture.rs @@ -1,8 +1,11 @@ -use std::hash::{Hash, Hasher}; -use std::num::NonZeroU8; +use std::{ + collections::hash_map::Entry::{Occupied, Vacant}, + hash::{Hash, Hasher}, + num::{NonZeroU32, NonZeroU8}, +}; use twox_hash::XxHash32; -use wgpu::util::DeviceExt; +use wgpu::{util::DeviceExt, ImageDataLayout}; use crate::{ get_app, @@ -15,11 +18,13 @@ use crate::{ pub(crate) struct TextureWithView { pub(crate) texture: wgpu::Texture, pub(crate) view: wgpu::TextureView, + pub(crate) format: wgpu::TextureFormat, + pub(crate) extent: wgpu::Extent3d, } impl TextureWithView { - fn new(texture: wgpu::Texture) -> Self { + fn new(texture: wgpu::Texture, format: wgpu::TextureFormat, extent: wgpu::Extent3d) -> Self { let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - Self { texture, view } + Self { texture, view, format, extent } } } @@ -37,24 +42,26 @@ pub(crate) fn create_static_texture_2d( label: &str, ) -> TextureRef { let gpu = &get_app().gpu; + let extent = wgpu::Extent3d { width, height, depth_or_array_layers: 1 }; + let wgpu_format = match format { + TextureFormat::RGBA8 => wgpu::TextureFormat::Rgba8Unorm, + TextureFormat::R8 => wgpu::TextureFormat::R8Unorm, + TextureFormat::R32Float => wgpu::TextureFormat::R32Float, + TextureFormat::DXT1 => wgpu::TextureFormat::Bc1RgbaUnorm, + TextureFormat::DXT3 => wgpu::TextureFormat::Bc3RgbaUnorm, + TextureFormat::DXT5 => wgpu::TextureFormat::Bc5RgUnorm, + TextureFormat::BPTC => wgpu::TextureFormat::Bc7RgbaUnorm, + _ => todo!(), + }; let texture = gpu.device.create_texture_with_data( &gpu.queue, &wgpu::TextureDescriptor { label: Some(label), - size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, + size: extent, mip_level_count: mips, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: match format { - TextureFormat::RGBA8 => wgpu::TextureFormat::Rgba8Unorm, - TextureFormat::R8 => wgpu::TextureFormat::R8Unorm, - TextureFormat::R32Float => wgpu::TextureFormat::R32Float, - TextureFormat::DXT1 => wgpu::TextureFormat::Bc1RgbaUnorm, - TextureFormat::DXT3 => wgpu::TextureFormat::Bc3RgbaUnorm, - TextureFormat::DXT5 => wgpu::TextureFormat::Bc5RgUnorm, - TextureFormat::BPTC => wgpu::TextureFormat::Bc7RgbaUnorm, - _ => todo!(), - }, + format: wgpu_format, usage: wgpu::TextureUsages::TEXTURE_BINDING, }, data, @@ -71,7 +78,60 @@ pub(crate) fn create_static_texture_2d( // Store texture and return reference let state = unsafe { STATE.as_mut().unwrap_unchecked() }; - state.textures.insert(id, TextureWithView::new(texture)); + match state.textures.entry(id) { + Occupied(entry) => panic!("Hash collision ({:x}) on texture creation: {}", id, label), + Vacant(entry) => { + state.textures.insert(id, TextureWithView::new(texture, wgpu_format, extent)); + } + } + TextureRef { id, render: false } +} + +pub(crate) fn create_dynamic_texture_2d( + width: u32, + height: u32, + mips: u32, + format: TextureFormat, + label: &str, +) -> TextureRef { + let gpu = &get_app().gpu; + let extent = wgpu::Extent3d { width, height, depth_or_array_layers: 1 }; + let wgpu_format = match format { + TextureFormat::RGBA8 => wgpu::TextureFormat::Rgba8Unorm, + TextureFormat::R8 => wgpu::TextureFormat::R8Unorm, + TextureFormat::R32Float => wgpu::TextureFormat::R32Float, + TextureFormat::DXT1 => wgpu::TextureFormat::Bc1RgbaUnorm, + TextureFormat::DXT3 => wgpu::TextureFormat::Bc3RgbaUnorm, + TextureFormat::DXT5 => wgpu::TextureFormat::Bc5RgUnorm, + TextureFormat::BPTC => wgpu::TextureFormat::Bc7RgbaUnorm, + _ => todo!(), + }; + let texture = gpu.device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size: extent, + mip_level_count: mips, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu_format, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + }); + + // Generate texture hash as ID + let mut hasher = XxHash32::with_seed(format.into()); + width.hash(&mut hasher); + height.hash(&mut hasher); + mips.hash(&mut hasher); + label.hash(&mut hasher); + let id = hasher.finish() as u32; + + // Store texture and return reference + let state = unsafe { STATE.as_mut().unwrap_unchecked() }; + match state.textures.entry(id) { + Occupied(entry) => panic!("Hash collision ({:x}) on texture creation: {}", id, label), + Vacant(entry) => { + state.textures.insert(id, TextureWithView::new(texture, wgpu_format, extent)); + } + } TextureRef { id, render: false } } @@ -85,28 +145,38 @@ pub(crate) fn create_render_texture( ) -> TextureRef { let gpu = &get_app().gpu; let color_texture = if color_bind_count > 0 { - Some(TextureWithView::new(gpu.device.create_texture(&wgpu::TextureDescriptor { - label: Some(format!("{} Color", label).as_str()), - size: wgpu::Extent3d { width, height, depth_or_array_layers: color_bind_count }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: gpu.config.color_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - }))) + let extent = wgpu::Extent3d { width, height, depth_or_array_layers: color_bind_count }; + Some(TextureWithView::new( + gpu.device.create_texture(&wgpu::TextureDescriptor { + label: Some(format!("{} Color", label).as_str()), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: gpu.config.color_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + }), + gpu.config.color_format, + extent, + )) } else { None }; let depth_texture = if depth_bind_count > 0 { - Some(TextureWithView::new(gpu.device.create_texture(&wgpu::TextureDescriptor { - label: Some(format!("{} Depth", label).as_str()), - size: wgpu::Extent3d { width, height, depth_or_array_layers: depth_bind_count }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: gpu.config.depth_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - }))) + let extent = wgpu::Extent3d { width, height, depth_or_array_layers: depth_bind_count }; + Some(TextureWithView::new( + gpu.device.create_texture(&wgpu::TextureDescriptor { + label: Some(format!("{} Depth", label).as_str()), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: gpu.config.depth_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + }), + gpu.config.depth_format, + extent, + )) } else { None }; @@ -146,10 +216,46 @@ pub(crate) fn create_render_texture( // Store texture and return reference let state = unsafe { STATE.as_mut().unwrap_unchecked() }; - state.render_textures.insert(id, RenderTexture { color_texture, depth_texture }); + match state.textures.entry(id) { + Occupied(entry) => panic!("Hash collision ({:x}) on texture creation: {}", id, label), + Vacant(entry) => { + state.render_textures.insert(id, RenderTexture { color_texture, depth_texture }); + } + } TextureRef { id, render: true } } +pub(crate) fn write_texture(handle: TextureRef, data: &[u8]) { + if handle.render { + panic!("Can't write to render texture"); + } + + let state = unsafe { STATE.as_mut().unwrap_unchecked() }; + if let Some(TextureWithView { texture, format, extent, .. }) = state.textures.get(&handle.id) { + state.queue.write_texture( + texture.as_image_copy(), + data, + ImageDataLayout { + offset: 0, + bytes_per_row: match format { + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::R32Float => { + NonZeroU32::new(extent.width * 4) + } + wgpu::TextureFormat::R8Unorm => NonZeroU32::new(extent.width), + _ => todo!("Unimplemented format for write_texture: {:?}", format), + }, + rows_per_image: match extent.depth_or_array_layers { + 1 => None, + _ => todo!("Unimplemented write_texture for 3D/2DArray textures"), + }, + }, + *extent, + ); + } else { + panic!("Failed to find texture {}", handle.id); + } +} + pub(crate) fn drop_texture(handle: TextureRef) { let state = unsafe { STATE.as_mut().unwrap_unchecked() }; if handle.render { diff --git a/Runtime/Graphics/CGraphics.hpp b/Runtime/Graphics/CGraphics.hpp index 18cfd415f..d1ee59308 100644 --- a/Runtime/Graphics/CGraphics.hpp +++ b/Runtime/Graphics/CGraphics.hpp @@ -47,6 +47,13 @@ inline std::shared_ptr new_static_texture_2d(uint32_t width, uint auto ref = aurora::shaders::create_static_texture_2d(width, height, mips, format, data, rlabel); return std::make_shared(ref); } +inline std::shared_ptr new_dynamic_texture_2d(uint32_t width, uint32_t height, uint32_t mips, + aurora::shaders::TextureFormat format, + std::string_view label) { + rust::Str rlabel{label.data(), label.size()}; + auto ref = aurora::shaders::create_dynamic_texture_2d(width, height, mips, format, rlabel); + return std::make_shared(ref); +} inline std::shared_ptr new_render_texture(uint32_t width, uint32_t height, uint32_t color_bind_count, uint32_t depth_bind_count, std::string_view label) { diff --git a/Runtime/Graphics/CMoviePlayer.cpp b/Runtime/Graphics/CMoviePlayer.cpp index 665e93486..154790cda 100644 --- a/Runtime/Graphics/CMoviePlayer.cpp +++ b/Runtime/Graphics/CMoviePlayer.cpp @@ -5,13 +5,10 @@ #include "Runtime/Graphics/CGraphics.hpp" #include -//#include #include namespace metaforce { -zeus::CMatrix4f g_PlatformMatrix; - /* used in the original to look up fixed-point dividends on a * MIDI-style volume scale (0-127) -> (n/0x8000) */ static const std::array StaticVolumeLookup = { @@ -26,8 +23,7 @@ static const std::array StaticVolumeLookup = { 0x55D6, 0x577E, 0x592B, 0x5ADC, 0x5C90, 0x5E49, 0x6006, 0x61C7, 0x638C, 0x6555, 0x6722, 0x68F4, 0x6AC9, 0x6CA2, 0x6E80, 0x7061, 0x7247, 0x7430, 0x761E, 0x7810, 0x7A06, 0x7C00, 0x7DFE, 0x8000}; -/* shared boo resources */ -//static boo::ObjToken YUVShaderPipeline; +/* shared resources */ static tjhandle TjHandle = nullptr; /* RSF audio state */ @@ -43,25 +39,9 @@ static g72x_state StaticStateRight = {}; /* THP SFX audio */ static float SfxVolume = 1.f; -static const char* BlockNames[] = {"SpecterViewBlock"}; -static const char* TexNames[] = {"texY", "texU", "texV"}; +void CMoviePlayer::Initialize() { TjHandle = tjInitDecompress(); } -void CMoviePlayer::Initialize() { -// switch (factory->platform()) { -// case boo::IGraphicsDataFactory::Platform::Vulkan: -// g_PlatformMatrix.m[1][1] = -1.f; -// break; -// default: -// break; -// } -// YUVShaderPipeline = hecl::conv->convert(Shader_CMoviePlayerShader{}); - TjHandle = tjInitDecompress(); -} - -void CMoviePlayer::Shutdown() { -// YUVShaderPipeline.reset(); - tjDestroy(TjHandle); -} +void CMoviePlayer::Shutdown() { tjDestroy(TjHandle); } void CMoviePlayer::THPHeader::swapBig() { magic = hecl::SBig(magic); @@ -201,51 +181,39 @@ CMoviePlayer::CMoviePlayer(const char* path, float preLoadSeconds, bool loop, bo if (xf0_preLoadFrames > 0) xa0_bufferQueue.reserve(xf0_preLoadFrames); - /* All set for GPU resources */ -// CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) { -// m_blockBuf = ctx.newDynamicBuffer(boo::BufferUse::Uniform, sizeof(m_viewVertBlock), 1); -// m_vertBuf = ctx.newDynamicBuffer(boo::BufferUse::Vertex, sizeof(TexShaderVert), 4); -// -// /* Allocate textures here (rather than at decode time) */ -// x80_textures.reserve(3); -// for (int i = 0; i < 3; ++i) { -// CTHPTextureSet& set = x80_textures.emplace_back(); -// if (deinterlace) { -// /* metaforce addition: this way interlaced THPs don't look horrible */ -// set.Y[0] = ctx.newDynamicTexture(x6c_videoInfo.width, x6c_videoInfo.height / 2, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// set.Y[1] = ctx.newDynamicTexture(x6c_videoInfo.width, x6c_videoInfo.height / 2, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// set.U = ctx.newDynamicTexture(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// set.V = ctx.newDynamicTexture(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// -// boo::ObjToken bufs[] = {m_blockBuf.get()}; -// for (int j = 0; j < 2; ++j) { -// boo::ObjToken texs[] = {set.Y[j].get(), set.U.get(), set.V.get()}; -// set.binding[j] = ctx.newShaderDataBinding(YUVShaderPipeline, m_vertBuf.get(), nullptr, nullptr, 1, bufs, -// nullptr, 3, texs, nullptr, nullptr); -// } -// } else { -// /* normal progressive presentation */ -// set.Y[0] = ctx.newDynamicTexture(x6c_videoInfo.width, x6c_videoInfo.height, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// set.U = ctx.newDynamicTexture(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// set.V = ctx.newDynamicTexture(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, boo::TextureFormat::I8, -// boo::TextureClampMode::Repeat); -// -// boo::ObjToken bufs[] = {m_blockBuf.get()}; -// boo::ObjToken texs[] = {set.Y[0].get(), set.U.get(), set.V.get()}; -// set.binding[0] = ctx.newShaderDataBinding(YUVShaderPipeline, m_vertBuf.get(), nullptr, nullptr, 1, bufs, -// nullptr, 3, texs, nullptr, nullptr); -// } -// if (xf4_25_hasAudio) -// set.audioBuf.reset(new s16[x28_thpHead.maxAudioSamples * 2]); -// } -// return true; -// } BooTrace); + /* Allocate textures here (rather than at decode time) */ + x80_textures.reserve(3); + for (int i = 0; i < 3; ++i) { + CTHPTextureSet& set = x80_textures.emplace_back(); + if (deinterlace) { + /* metaforce addition: this way interlaced THPs don't look horrible */ + set.Y[0] = aurora::new_dynamic_texture_2d(x6c_videoInfo.width, x6c_videoInfo.height / 2, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} Y[0]"), path, i)); + set.Y[1] = aurora::new_dynamic_texture_2d(x6c_videoInfo.width, x6c_videoInfo.height / 2, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} Y[1]"), path, i)); + set.U = aurora::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} U"), path, i)); + set.V = aurora::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} V"), path, i)); + } else { + /* normal progressive presentation */ + set.Y[0] = aurora::new_dynamic_texture_2d(x6c_videoInfo.width, x6c_videoInfo.height, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} Y"), path, i)); + set.U = aurora::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} U"), path, i)); + set.V = aurora::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, + aurora::shaders::TextureFormat::R8, + fmt::format(FMT_STRING("Movie {} Texture Set {} V"), path, i)); + } + if (xf4_25_hasAudio) + set.audioBuf.reset(new s16[x28_thpHead.maxAudioSamples * 2]); + } /* Temporary planar YUV decode buffer, resulting planes copied to Boo */ m_yuvBuf.reset(new uint8_t[tjBufSizeYUV(x6c_videoInfo.width, x6c_videoInfo.height, TJ_420)]); @@ -253,14 +221,8 @@ CMoviePlayer::CMoviePlayer(const char* path, float preLoadSeconds, bool loop, bo /* Schedule initial read */ PostDVDReadRequestIfNeeded(); - m_frame[0].m_uv = {0.f, 0.f}; - m_frame[1].m_uv = {0.f, 1.f}; - m_frame[2].m_uv = {1.f, 0.f}; - m_frame[3].m_uv = {1.f, 1.f}; - SetFrame({-0.5f, 0.5f, 0.f}, {-0.5f, -0.5f, 0.f}, {0.5f, -0.5f, 0.f}, {0.5f, 0.5f, 0.f}); - - m_viewVertBlock.finalAssign(m_viewVertBlock); -// m_blockBuf->load(&m_viewVertBlock, sizeof(m_viewVertBlock)); + m_hpad = 0.5f; + m_vpad = 0.5f; } void CMoviePlayer::SetStaticAudioVolume(int vol) { @@ -399,13 +361,9 @@ void CMoviePlayer::Rewind() { xe8_curSeconds = 0.f; } -void CMoviePlayer::SetFrame(const zeus::CVector3f& a, const zeus::CVector3f& b, const zeus::CVector3f& c, - const zeus::CVector3f& d) { - m_frame[0].m_pos = a; - m_frame[1].m_pos = b; - m_frame[2].m_pos = d; - m_frame[3].m_pos = c; -// m_vertBuf->load(m_frame, sizeof(m_frame)); +void CMoviePlayer::SetFrame(float hpad, float vpad) { + m_hpad = hpad; + m_vpad = vpad; } void CMoviePlayer::DrawFrame() { @@ -415,8 +373,8 @@ void CMoviePlayer::DrawFrame() { /* draw appropriate field */ CTHPTextureSet& tex = x80_textures[xd0_drawTexSlot]; -// CGraphics::SetShaderDataBinding(tex.binding[m_deinterlace ? (xfc_fieldIndex != 0) : 0]); -// CGraphics::DrawArray(0, 4); + aurora::shaders::queue_movie_player(tex.Y[m_deinterlace ? (xfc_fieldIndex != 0) : 0]->ref, tex.U->ref, tex.V->ref, + zeus::skWhite, m_hpad, m_vpad); /* ensure second field is being displayed by VI to signal advance * (faked in metaforce with continuous xor) */ @@ -532,28 +490,23 @@ void CMoviePlayer::DecodeFromRead(const void* data) { if (m_deinterlace) { /* Deinterlace into 2 discrete 60-fps half-res textures */ -// u8* mappedData = (u8*)tex.Y[0]->map(planeSizeHalf); -// for (unsigned y = 0; y < x6c_videoInfo.height / 2; ++y) { -// memmove(mappedData + x6c_videoInfo.width * y, m_yuvBuf.get() + x6c_videoInfo.width * (y * 2), -// x6c_videoInfo.width); -// } -// tex.Y[0]->unmap(); -// -// mappedData = (u8*)tex.Y[1]->map(planeSizeHalf); -// for (unsigned y = 0; y < x6c_videoInfo.height / 2; ++y) { -// memmove(mappedData + x6c_videoInfo.width * y, m_yuvBuf.get() + x6c_videoInfo.width * (y * 2 + 1), -// x6c_videoInfo.width); -// } -// tex.Y[1]->unmap(); -// -// tex.U->load(m_yuvBuf.get() + planeSize, planeSizeQuarter); -// tex.V->load(m_yuvBuf.get() + planeSize + planeSizeQuarter, planeSizeQuarter); + auto buffer = std::make_unique(planeSizeHalf); + for (unsigned y = 0; y < x6c_videoInfo.height / 2; ++y) { + memcpy(buffer.get() + x6c_videoInfo.width * y, m_yuvBuf.get() + x6c_videoInfo.width * (y * 2), + x6c_videoInfo.width); + } + aurora::shaders::write_texture(tex.Y[0]->ref, {buffer.get(), planeSizeHalf}); + for (unsigned y = 0; y < x6c_videoInfo.height / 2; ++y) { + memmove(buffer.get() + x6c_videoInfo.width * y, m_yuvBuf.get() + x6c_videoInfo.width * (y * 2 + 1), + x6c_videoInfo.width); + } + aurora::shaders::write_texture(tex.Y[1]->ref, {buffer.get(), planeSizeHalf}); } else { /* Direct planar load */ -// tex.Y[0]->load(m_yuvBuf.get(), planeSize); -// tex.U->load(m_yuvBuf.get() + planeSize, planeSizeQuarter); -// tex.V->load(m_yuvBuf.get() + planeSize + planeSizeQuarter, planeSizeQuarter); + aurora::shaders::write_texture(tex.Y[0]->ref, {m_yuvBuf.get(), planeSize}); } + aurora::shaders::write_texture(tex.U->ref, {m_yuvBuf.get() + planeSize, planeSizeQuarter}); + aurora::shaders::write_texture(tex.V->ref, {m_yuvBuf.get() + planeSize + planeSizeQuarter, planeSizeQuarter}); break; } diff --git a/Runtime/Graphics/CMoviePlayer.hpp b/Runtime/Graphics/CMoviePlayer.hpp index 4d7b8a6e6..ff4df7f8a 100644 --- a/Runtime/Graphics/CMoviePlayer.hpp +++ b/Runtime/Graphics/CMoviePlayer.hpp @@ -12,8 +12,6 @@ namespace metaforce { -extern zeus::CMatrix4f g_PlatformMatrix; - class CMoviePlayer : public CDvdFile { public: enum class EPlayMode { Stopped, Playing }; @@ -77,7 +75,6 @@ private: u32 playedSamples = 0; u32 audioSamples = 0; std::unique_ptr audioBuf; -// boo::ObjToken binding[2]; }; std::vector x80_textures; std::unique_ptr x90_requestBuf; @@ -109,31 +106,8 @@ private: u32 xfc_fieldIndex = 0; std::unique_ptr m_yuvBuf; - - struct TexShaderVert { - zeus::CVector3f m_pos; - zeus::CVector2f m_uv; - }; - struct ViewBlock { - zeus::CMatrix4f m_mv; - zeus::CColor m_color = zeus::skWhite; - void setViewRect(const aurora::shaders::ClipRect& root, const aurora::shaders::ClipRect& sub) { - m_mv[0][0] = 2.0f / root.width; - m_mv[1][1] = 2.0f / root.height; - m_mv[3][0] = sub.x * m_mv[0][0] - 1.0f; - m_mv[3][1] = sub.y * m_mv[1][1] - 1.0f; - } - void finalAssign(const ViewBlock& other) { - m_mv = g_PlatformMatrix * other.m_mv; - m_color = other.m_color; - } - }; - - ViewBlock m_viewVertBlock; -// boo::ObjToken m_blockBuf; -// boo::ObjToken m_vertBuf; - - TexShaderVert m_frame[4]; + float m_hpad; + float m_vpad; static u32 THPAudioDecode(s16* buffer, const u8* audioFrame, bool stereo); void DecodeFromRead(const void* data); @@ -161,7 +135,7 @@ public: float GetPlayedSeconds() const { return xdc_frameRem + xe8_curSeconds; } float GetTotalSeconds() const { return xe4_totalSeconds; } void SetPlayMode(EPlayMode mode) { xe0_playMode = mode; } - void SetFrame(const zeus::CVector3f& a, const zeus::CVector3f& b, const zeus::CVector3f& c, const zeus::CVector3f& d); + void SetFrame(float hpad, float vpad); void DrawFrame(); void Update(float dt); std::pair GetVideoDimensions() const { return {x6c_videoInfo.width, x6c_videoInfo.height}; } diff --git a/Runtime/MP1/CCredits.cpp b/Runtime/MP1/CCredits.cpp index 497118e5d..90ba14edb 100644 --- a/Runtime/MP1/CCredits.cpp +++ b/Runtime/MP1/CCredits.cpp @@ -212,7 +212,7 @@ void CCredits::DrawVideo() { if (x28_ && x28_->GetIsFullyCached()) { /* Render movie */ - x28_->SetFrame({-hPad, vPad, 0.f}, {-hPad, -vPad, 0.f}, {hPad, -vPad, 0.f}, {hPad, vPad, 0.f}); + x28_->SetFrame(hPad, vPad); x28_->DrawFrame(); if (x5c_27_ || x5c_28_) { float alpha = x58_ / g_tweakGui->x310_; diff --git a/Runtime/MP1/CFrontEndUI.cpp b/Runtime/MP1/CFrontEndUI.cpp index a6d5915a7..ee788b66f 100644 --- a/Runtime/MP1/CFrontEndUI.cpp +++ b/Runtime/MP1/CFrontEndUI.cpp @@ -56,7 +56,7 @@ struct FEMovie { bool loop; }; -constexpr std::array FEMovies{{ +constexpr std::array FEMovies{{ {"Video/00_first_start.thp", false}, {"Video/01_startloop.thp", true}, {"Video/02_start_fileselect_A.thp", false}, @@ -65,7 +65,6 @@ constexpr std::array FEMovies{{ {"Video/06_fileselect_GBA.thp", false}, {"Video/07_GBAloop.thp", true}, {"Video/08_GBA_fileselect.thp", false}, - {"Video/08_GBA_fileselect.thp", false}, }}; constexpr SObjectTag g_DefaultWorldTag = {FOURCC('MLVL'), 0x158efe17}; @@ -2021,7 +2020,7 @@ void CFrontEndUI::Draw() { if (xcc_curMoviePtr && xcc_curMoviePtr->GetIsFullyCached()) { /* Render movie */ - xcc_curMoviePtr->SetFrame({-hPad, vPad, 0.f}, {-hPad, -vPad, 0.f}, {hPad, -vPad, 0.f}, {hPad, vPad, 0.f}); + xcc_curMoviePtr->SetFrame(hPad, vPad); xcc_curMoviePtr->DrawFrame(); } @@ -2038,7 +2037,7 @@ void CFrontEndUI::Draw() { xe4_fusionBonusFrme->Draw(); } - if (x64_pressStartAlpha > 0.f && x38_pressStart.IsLoaded() && m_pressStartQuad) { + if (x64_pressStartAlpha > 0.f && x38_pressStart.IsLoaded()) { /* Render "Press Start" */ const zeus::CRectangle rect(0.5f - x38_pressStart->GetWidth() / 2.f / 640.f * hPad, 0.5f + (x38_pressStart->GetHeight() / 2.f - 240.f + 72.f) / 480.f * vPad, @@ -2047,7 +2046,7 @@ void CFrontEndUI::Draw() { zeus::CColor color = zeus::skWhite; color.a() = x64_pressStartAlpha; aurora::shaders::queue_textured_quad( - aurora::shaders::CameraFilterType::Blend, + aurora::shaders::CameraFilterType::Add, x38_pressStart->GetTexture()->ref, aurora::shaders::ZTest::None, color, @@ -2130,9 +2129,6 @@ bool CFrontEndUI::PumpLoad() { if (!x44_frontendAudioGrp.IsLoaded()) return false; - /* Ready to construct texture quads */ - m_pressStartQuad.emplace(EFilterType::Add, x38_pressStart); - return true; } diff --git a/Runtime/MP1/CFrontEndUI.hpp b/Runtime/MP1/CFrontEndUI.hpp index c787be120..c0c3a9402 100644 --- a/Runtime/MP1/CFrontEndUI.hpp +++ b/Runtime/MP1/CFrontEndUI.hpp @@ -330,7 +330,7 @@ private: float x64_pressStartAlpha = 0.f; float x68_musicVol = 1.f; u32 x6c_; - std::array, 9> x70_menuMovies; + std::array, 8> x70_menuMovies; EMenuMovie xb8_curMovie = EMenuMovie::Stopped; int xbc_nextAttract = 0; int xc0_attractCount = 0; @@ -350,7 +350,6 @@ private: CStaticAudioPlayer* xf4_curAudio = nullptr; CColoredQuadFilter m_fadeToBlack{EFilterType::Blend}; - std::optional m_pressStartQuad; std::unique_ptr m_touchBar;