diff --git a/render/wgpu/shaders/bitmap.wgsl b/render/wgpu/shaders/bitmap.wgsl index 7b3b4b803..0992dc1e4 100644 --- a/render/wgpu/shaders/bitmap.wgsl +++ b/render/wgpu/shaders/bitmap.wgsl @@ -30,6 +30,5 @@ fn main_fragment(in: VertexOutput) -> [[location(0)]] vec4 { color = color * transforms.mult_color + transforms.add_color; color = vec4(color.rgb * color.a, color.a); } - let out = color; - return output(out); + return color; } diff --git a/render/wgpu/shaders/color.wgsl b/render/wgpu/shaders/color.wgsl index 370546f1e..c97687b60 100644 --- a/render/wgpu/shaders/color.wgsl +++ b/render/wgpu/shaders/color.wgsl @@ -13,6 +13,5 @@ fn main_vertex(in: VertexInput) -> VertexOutput { [[stage(fragment)]] fn main_fragment(in: VertexOutput) -> [[location(0)]] vec4 { - let out = in.color * transforms.mult_color + transforms.add_color; - return output(out); + return in.color * transforms.mult_color + transforms.add_color; } diff --git a/render/wgpu/shaders/copy_srgb.wgsl b/render/wgpu/shaders/copy_srgb.wgsl new file mode 100644 index 000000000..336d75940 --- /dev/null +++ b/render/wgpu/shaders/copy_srgb.wgsl @@ -0,0 +1,26 @@ +/// Shader used for drawing bitmap fills. + +struct VertexOutput { + [[builtin(position)]] position: vec4; + [[location(0)]] uv: vec2; +}; + +[[group(2), binding(0)]] +var textureTransforms: TextureTransforms; +[[group(2), binding(1)]] +var texture: texture_2d; +[[group(3), binding(0)]] +var texture_sampler: sampler; + +[[stage(vertex)]] +fn main_vertex(in: VertexInput) -> VertexOutput { + let matrix = textureTransforms.matrix; + let uv = (mat3x3(matrix[0].xyz, matrix[1].xyz, matrix[2].xyz) * vec3(in.position, 1.0)).xy; + let pos = globals.view_matrix * transforms.world_matrix * vec4(in.position.x, in.position.y, 0.0, 1.0); + return VertexOutput(pos, uv); +} + +[[stage(fragment)]] +fn main_fragment(in: VertexOutput) -> [[location(0)]] vec4 { + return srgb_to_linear(textureSample(texture, texture_sampler, in.uv)); +} diff --git a/render/wgpu/shaders/gradient.wgsl b/render/wgpu/shaders/gradient.wgsl index 9f23c1dbc..d0661056a 100644 --- a/render/wgpu/shaders/gradient.wgsl +++ b/render/wgpu/shaders/gradient.wgsl @@ -101,6 +101,5 @@ fn main_fragment(in: VertexOutput) -> [[location(0)]] vec4 { if( gradient.interpolation != 0 ) { color = linear_to_srgb(color); } - let out = color * transforms.mult_color + transforms.add_color; - return output(out); + return color * transforms.mult_color + transforms.add_color; } diff --git a/render/wgpu/shaders/output_linear.wgsl b/render/wgpu/shaders/output_linear.wgsl deleted file mode 100644 index fd95efcd4..000000000 --- a/render/wgpu/shaders/output_linear.wgsl +++ /dev/null @@ -1,3 +0,0 @@ -fn output(srgb: vec4) -> vec4 { - return srgb_to_linear(srgb); -} diff --git a/render/wgpu/shaders/output_srgb.wgsl b/render/wgpu/shaders/output_srgb.wgsl deleted file mode 100644 index b5f6042c0..000000000 --- a/render/wgpu/shaders/output_srgb.wgsl +++ /dev/null @@ -1,3 +0,0 @@ -fn output(srgb: vec4) -> vec4 { - return srgb; -} diff --git a/render/wgpu/src/lib.rs b/render/wgpu/src/lib.rs index ae3bcaa9a..e23577bd3 100644 --- a/render/wgpu/src/lib.rs +++ b/render/wgpu/src/lib.rs @@ -44,6 +44,7 @@ pub struct Descriptors { pub info: wgpu::AdapterInfo, pub limits: wgpu::Limits, pub surface_format: wgpu::TextureFormat, + frame_buffer_format: wgpu::TextureFormat, queue: wgpu::Queue, globals: Globals, uniform_buffers: UniformBuffer, @@ -83,9 +84,41 @@ impl Descriptors { uniform_buffer_layout, limits.min_uniform_buffer_offset_alignment, ); + + // We want to render directly onto a linear render target to avoid any gamma correction. + // If our surface is sRGB, render to a linear texture and than copy over to the surface. + // Remove Srgb from texture format. + let frame_buffer_format = match surface_format { + wgpu::TextureFormat::Rgba8UnormSrgb => wgpu::TextureFormat::Rgba8Unorm, + wgpu::TextureFormat::Bgra8UnormSrgb => wgpu::TextureFormat::Bgra8Unorm, + wgpu::TextureFormat::Bc1RgbaUnormSrgb => wgpu::TextureFormat::Bc1RgbaUnorm, + wgpu::TextureFormat::Bc2RgbaUnormSrgb => wgpu::TextureFormat::Bc2RgbaUnorm, + wgpu::TextureFormat::Bc3RgbaUnormSrgb => wgpu::TextureFormat::Bc3RgbaUnorm, + wgpu::TextureFormat::Bc7RgbaUnormSrgb => wgpu::TextureFormat::Bc7RgbaUnorm, + wgpu::TextureFormat::Etc2Rgb8UnormSrgb => wgpu::TextureFormat::Etc2Rgb8Unorm, + wgpu::TextureFormat::Etc2Rgb8A1UnormSrgb => wgpu::TextureFormat::Etc2Rgb8A1Unorm, + wgpu::TextureFormat::Etc2Rgba8UnormSrgb => wgpu::TextureFormat::Etc2Rgba8Unorm, + wgpu::TextureFormat::Astc4x4RgbaUnormSrgb => wgpu::TextureFormat::Astc4x4RgbaUnorm, + wgpu::TextureFormat::Astc5x4RgbaUnormSrgb => wgpu::TextureFormat::Astc5x4RgbaUnorm, + wgpu::TextureFormat::Astc5x5RgbaUnormSrgb => wgpu::TextureFormat::Astc5x5RgbaUnorm, + wgpu::TextureFormat::Astc6x5RgbaUnormSrgb => wgpu::TextureFormat::Astc6x5RgbaUnorm, + wgpu::TextureFormat::Astc6x6RgbaUnormSrgb => wgpu::TextureFormat::Astc6x6RgbaUnorm, + wgpu::TextureFormat::Astc8x5RgbaUnormSrgb => wgpu::TextureFormat::Astc8x5RgbaUnorm, + wgpu::TextureFormat::Astc8x6RgbaUnormSrgb => wgpu::TextureFormat::Astc8x6RgbaUnorm, + wgpu::TextureFormat::Astc10x5RgbaUnormSrgb => wgpu::TextureFormat::Astc10x5RgbaUnorm, + wgpu::TextureFormat::Astc10x6RgbaUnormSrgb => wgpu::TextureFormat::Astc10x6RgbaUnorm, + wgpu::TextureFormat::Astc8x8RgbaUnormSrgb => wgpu::TextureFormat::Astc8x8RgbaUnorm, + wgpu::TextureFormat::Astc10x8RgbaUnormSrgb => wgpu::TextureFormat::Astc10x8RgbaUnorm, + wgpu::TextureFormat::Astc10x10RgbaUnormSrgb => wgpu::TextureFormat::Astc10x10RgbaUnorm, + wgpu::TextureFormat::Astc12x10RgbaUnormSrgb => wgpu::TextureFormat::Astc12x10RgbaUnorm, + wgpu::TextureFormat::Astc12x12RgbaUnormSrgb => wgpu::TextureFormat::Astc12x12RgbaUnorm, + _ => surface_format, + }; + let pipelines = Pipelines::new( &device, surface_format, + frame_buffer_format, msaa_sample_count, bitmap_samplers.layout(), globals.layout(), @@ -97,6 +130,7 @@ impl Descriptors { info, limits, surface_format, + frame_buffer_format, queue, globals, uniform_buffers, @@ -112,6 +146,8 @@ pub struct WgpuRenderBackend { target: T, frame_buffer_view: wgpu::TextureView, depth_texture_view: wgpu::TextureView, + copy_srgb_view: wgpu::TextureView, + copy_srgb_bind_group: wgpu::BindGroup, current_frame: Option>, meshes: Vec, mask_state: MaskState, @@ -371,7 +407,7 @@ impl WgpuRenderBackend { mip_level_count: 1, sample_count: descriptors.msaa_sample_count, dimension: wgpu::TextureDimension::D2, - format: target.format(), + format: descriptors.frame_buffer_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); let frame_buffer_view = frame_buffer.create_view(&Default::default()); @@ -391,6 +427,40 @@ impl WgpuRenderBackend { let (quad_vbo, quad_ibo, quad_tex_transforms) = create_quad_buffers(&descriptors.device); + let copy_srgb_buffer = descriptors.device.create_texture(&wgpu::TextureDescriptor { + label: create_debug_label!("Copy sRGB framebuffer texture").as_deref(), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: descriptors.frame_buffer_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + }); + let copy_srgb_view = copy_srgb_buffer.create_view(&Default::default()); + let copy_srgb_bind_group = + descriptors + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &descriptors.pipelines.bitmap_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &quad_tex_transforms, + offset: 0, + size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(©_srgb_view), + }, + ], + label: create_debug_label!("Copy sRGB bind group").as_deref(), + }); + descriptors .globals .set_resolution(target.width(), target.height()); @@ -400,6 +470,8 @@ impl WgpuRenderBackend { target, frame_buffer_view, depth_texture_view, + copy_srgb_view, + copy_srgb_bind_group, current_frame: None, meshes: Vec::new(), shape_tessellator: ShapeTessellator::new(), @@ -767,21 +839,23 @@ impl RenderBackend for WgpuRenderBackend { self.target.resize(&self.descriptors.device, width, height); + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let label = create_debug_label!("Framebuffer texture"); let frame_buffer = self .descriptors .device .create_texture(&wgpu::TextureDescriptor { label: label.as_deref(), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, + size, mip_level_count: 1, sample_count: self.descriptors.msaa_sample_count, dimension: wgpu::TextureDimension::D2, - format: self.target.format(), + format: self.descriptors.frame_buffer_format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); self.frame_buffer_view = frame_buffer.create_view(&Default::default()); @@ -792,11 +866,7 @@ impl RenderBackend for WgpuRenderBackend { .device .create_texture(&wgpu::TextureDescriptor { label: label.as_deref(), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, + size, mip_level_count: 1, sample_count: self.descriptors.msaa_sample_count, dimension: wgpu::TextureDimension::D2, @@ -804,6 +874,45 @@ impl RenderBackend for WgpuRenderBackend { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); self.depth_texture_view = depth_texture.create_view(&Default::default()); + + let copy_srgb_buffer = self + .descriptors + .device + .create_texture(&wgpu::TextureDescriptor { + label: create_debug_label!("Copy sRGB framebuffer texture").as_deref(), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: self.descriptors.frame_buffer_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + }); + self.copy_srgb_view = copy_srgb_buffer.create_view(&Default::default()); + self.copy_srgb_bind_group = + self.descriptors + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &self.descriptors.pipelines.bitmap_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &self.quad_tex_transforms, + offset: 0, + size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&self.copy_srgb_view), + }, + ], + label: create_debug_label!("Copy sRBG bind group").as_deref(), + }); + self.descriptors.globals.set_resolution(width, height); } @@ -910,10 +1019,15 @@ impl RenderBackend for WgpuRenderBackend { .globals .update_uniform(&self.descriptors.device, &mut frame_data.0); - let (color_view, resolve_target) = if self.descriptors.msaa_sample_count >= 2 { - (&self.frame_buffer_view, Some(frame_data.1.view())) - } else { - (frame_data.1.view(), None) + // Use intermediate render targets when resolving MSAA or copying from linear-to-sRGB texture. + let (color_view, resolve_target) = match ( + self.descriptors.frame_buffer_format != self.descriptors.surface_format, + self.descriptors.msaa_sample_count >= 2, + ) { + (false, false) => (frame_data.1.view(), None), + (false, true) => (&self.frame_buffer_view, Some(frame_data.1.view())), + (true, false) => (&self.copy_srgb_view, None), + (true, true) => (&self.frame_buffer_view, Some(&self.copy_srgb_view)), }; let render_pass = frame_data.0.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -1216,15 +1330,81 @@ impl RenderBackend for WgpuRenderBackend { fn end_frame(&mut self) { if let Some(frame) = self.current_frame.take() { - // Finalize render pass. - drop(frame.render_pass); - self.descriptors.uniform_buffers.finish(); let draw_encoder = frame.frame_data.0; - let uniform_encoder = frame.frame_data.2; + let mut uniform_encoder = frame.frame_data.2; + let render_pass = frame.render_pass; + // Finalize render pass. + drop(render_pass); + + // If we have an sRGB surface, copy from our linear intermediate buffer to the sRGB surface. + let command_buffers = if self.descriptors.frame_buffer_format + != self.descriptors.surface_format + { + let mut copy_encoder = self.descriptors.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: create_debug_label!("Frame copy command encoder").as_deref(), + }, + ); + + let mut render_pass = copy_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachment { + view: &frame.frame_data.1.view(), + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: true, + }, + resolve_target: None, + }], + depth_stencil_attachment: None, + label: None, + }); + + render_pass.set_pipeline(&self.descriptors.pipelines.copy_srgb_pipeline); + render_pass.set_bind_group(0, self.descriptors.globals.bind_group(), &[]); + self.descriptors.uniform_buffers.write_uniforms( + &self.descriptors.device, + &mut uniform_encoder, + &mut render_pass, + 1, + &Transforms { + world_matrix: [ + [self.target.width() as f32, 0.0, 0.0, 0.0], + [0.0, self.target.height() as f32, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + color_adjustments: ColorAdjustments { + mult_color: [1.0, 1.0, 1.0, 1.0], + add_color: [0.0, 0.0, 0.0, 0.0], + }, + }, + ); + render_pass.set_bind_group(2, &self.copy_srgb_bind_group, &[]); + render_pass.set_bind_group( + 3, + self.descriptors + .bitmap_samplers + .get_bind_group(false, false), + &[], + ); + render_pass.set_vertex_buffer(0, self.quad_vbo.slice(..)); + render_pass.set_index_buffer(self.quad_ibo.slice(..), wgpu::IndexFormat::Uint32); + render_pass.draw_indexed(0..6, 0, 0..1); + drop(render_pass); + vec![ + uniform_encoder.finish(), + draw_encoder.finish(), + copy_encoder.finish(), + ] + } else { + vec![uniform_encoder.finish(), draw_encoder.finish()] + }; + + self.descriptors.uniform_buffers.finish(); self.target.submit( &self.descriptors.device, &self.descriptors.queue, - vec![uniform_encoder.finish(), draw_encoder.finish()], + command_buffers, frame.frame_data.1, ); } diff --git a/render/wgpu/src/pipelines.rs b/render/wgpu/src/pipelines.rs index f1931f12b..d604c7d47 100644 --- a/render/wgpu/src/pipelines.rs +++ b/render/wgpu/src/pipelines.rs @@ -16,6 +16,9 @@ pub struct Pipelines { pub gradient_pipelines: ShapePipeline, pub gradient_layout: wgpu::BindGroupLayout, + + pub copy_srgb_pipeline: wgpu::RenderPipeline, + pub copy_srgb_layout: wgpu::BindGroupLayout, } impl ShapePipeline { @@ -28,31 +31,20 @@ impl Pipelines { pub fn new( device: &wgpu::Device, surface_format: wgpu::TextureFormat, + frame_buffer_format: wgpu::TextureFormat, msaa_sample_count: u32, sampler_layout: &wgpu::BindGroupLayout, globals_layout: &wgpu::BindGroupLayout, dynamic_uniforms_layout: &wgpu::BindGroupLayout, ) -> Result { - // If the surface is sRGB, the GPU will automatically convert colors from linear to sRGB, - // so our shader should output linear colors. - let output_srgb = !surface_format.describe().srgb; - let color_shader = create_shader( + let color_shader = create_shader(device, "color", include_str!("../shaders/color.wgsl")); + let bitmap_shader = create_shader(device, "bitmap", include_str!("../shaders/bitmap.wgsl")); + let gradient_shader = + create_shader(device, "gradient", include_str!("../shaders/gradient.wgsl")); + let copy_srgb_shader = create_shader( device, - "color", - include_str!("../shaders/color.wgsl"), - output_srgb, - ); - let bitmap_shader = create_shader( - device, - "bitmap", - include_str!("../shaders/bitmap.wgsl"), - output_srgb, - ); - let gradient_shader = create_shader( - device, - "gradient", - include_str!("../shaders/gradient.wgsl"), - output_srgb, + "copy sRGB", + include_str!("../shaders/copy_srgb.wgsl"), ); let vertex_buffers_description = [wgpu::VertexBufferLayout { @@ -66,7 +58,7 @@ impl Pipelines { let color_pipelines = create_color_pipelines( device, - surface_format, + frame_buffer_format, &color_shader, msaa_sample_count, &vertex_buffers_description, @@ -104,7 +96,7 @@ impl Pipelines { let bitmap_pipelines = create_bitmap_pipeline( device, - surface_format, + frame_buffer_format, &bitmap_shader, msaa_sample_count, &vertex_buffers_description, @@ -144,7 +136,7 @@ impl Pipelines { let gradient_pipelines = create_gradient_pipeline( device, - surface_format, + frame_buffer_format, &gradient_shader, msaa_sample_count, &vertex_buffers_description, @@ -153,41 +145,82 @@ impl Pipelines { &gradient_bind_layout, ); + let copy_srgb_bind_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + ], + label: create_debug_label!("Copy sRGB bind group layout").as_deref(), + }); + let copy_texture_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: create_debug_label!("Copy sRGB pipeline layout").as_deref(), + bind_group_layouts: &[ + globals_layout, + dynamic_uniforms_layout, + &bitmap_bind_layout, + sampler_layout, + ], + push_constant_ranges: &[], + }); + let copy_srgb_pipeline = device.create_render_pipeline(&create_pipeline_descriptor( + create_debug_label!("Copy sRGB pipeline").as_deref(), + ©_srgb_shader, + ©_srgb_shader, + ©_texture_pipeline_layout, + None, + &[wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: Default::default(), + }], + &vertex_buffers_description, + 1, + )); + Ok(Self { color_pipelines, bitmap_pipelines, bitmap_layout: bitmap_bind_layout, gradient_pipelines, gradient_layout: gradient_bind_layout, + copy_srgb_pipeline, + copy_srgb_layout: copy_srgb_bind_layout, }) } } /// Builds a `wgpu::ShaderModule` the given WGSL source in `src`. /// -/// The source is prepended with common code in `common.wgsl` and sRGB/linear conversions in -/// `output_srgb.wgsl`/`output_linear.wgsl`, simulating a `#include` preprocessor. We could -/// possibly does this as an offline build step instead. +/// The source is prepended with common code in `common.wgsl`, simulating a `#include` preprocessor. +/// We could possibly does this as an offline build step instead. fn create_shader( device: &wgpu::Device, name: &'static str, src: &'static str, - output_srgb: bool, ) -> wgpu::ShaderModule { const COMMON_SRC: &str = include_str!("../shaders/common.wgsl"); - const OUTPUT_LINEAR_SRC: &str = include_str!("../shaders/output_linear.wgsl"); - const OUTPUT_SRGB_SRC: &str = include_str!("../shaders/output_srgb.wgsl"); - - let src = if output_srgb { - [COMMON_SRC, OUTPUT_SRGB_SRC, src].concat() - } else { - [COMMON_SRC, OUTPUT_LINEAR_SRC, src].concat() - }; - let label = create_debug_label!( - "Shader {} ({})", - name, - if output_srgb { "sRGB" } else { "linear" } - ); + let src = [COMMON_SRC, src].concat(); + let label = create_debug_label!("Shader {}", name,); let desc = wgpu::ShaderModuleDescriptor { label: label.as_deref(), source: wgpu::ShaderSource::Wgsl(src.into()),