Skip to main content

wgpu_native_texture_interop/raw_gl/
texture_normalizer.rs

1//! GPU-based texture normalization pass.
2//!
3//! Converts imported textures to a canonical format (Rgba8Unorm, top-left origin)
4//! via a fullscreen-triangle render pass. Handles BGRA-to-RGBA conversion and
5//! Y-flip in a single draw call.
6
7use dpi::PhysicalSize;
8
9/// Caches the wgpu pipeline state for normalizing imported textures.
10///
11/// Create once and reuse across frames to avoid repeated shader compilation.
12pub struct ImportedTextureNormalizer {
13    sampler: wgpu::Sampler,
14    render_pipeline: wgpu::RenderPipeline,
15    bind_group_layout: wgpu::BindGroupLayout,
16}
17
18impl ImportedTextureNormalizer {
19    pub fn new(device: &wgpu::Device) -> Self {
20        let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
21            label: Some("texture-normalize-vs"),
22            source: wgpu::ShaderSource::Wgsl(
23                r#"
24                @vertex
25                fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
26                    let uv = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
27                    return vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
28                }
29            "#
30                .into(),
31            ),
32        });
33
34        let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
35            label: Some("texture-normalize-fs"),
36            source: wgpu::ShaderSource::Wgsl(
37                r#"
38                @group(0) @binding(0) var source_texture: texture_2d<f32>;
39                @group(0) @binding(1) var source_sampler: sampler;
40
41                @fragment
42                fn fs_main(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
43                    let size = textureDimensions(source_texture);
44                    let uv = position.xy / vec2<f32>(f32(size.x), f32(size.y));
45                    let flipped_uv = vec2<f32>(uv.x, 1.0 - uv.y);
46                    return textureSample(source_texture, source_sampler, flipped_uv);
47                }
48            "#
49                .into(),
50            ),
51        });
52
53        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
54            label: Some("texture-normalize-bind-group-layout"),
55            entries: &[
56                wgpu::BindGroupLayoutEntry {
57                    binding: 0,
58                    visibility: wgpu::ShaderStages::FRAGMENT,
59                    ty: wgpu::BindingType::Texture {
60                        multisampled: false,
61                        view_dimension: wgpu::TextureViewDimension::D2,
62                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
63                    },
64                    count: None,
65                },
66                wgpu::BindGroupLayoutEntry {
67                    binding: 1,
68                    visibility: wgpu::ShaderStages::FRAGMENT,
69                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
70                    count: None,
71                },
72            ],
73        });
74
75        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
76            label: Some("texture-normalize-sampler"),
77            compare: None,
78            border_color: None,
79            lod_min_clamp: 0.0,
80            lod_max_clamp: 0.0,
81            anisotropy_clamp: 1,
82            mag_filter: wgpu::FilterMode::Linear,
83            min_filter: wgpu::FilterMode::Linear,
84            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
85            address_mode_u: wgpu::AddressMode::ClampToEdge,
86            address_mode_v: wgpu::AddressMode::ClampToEdge,
87            address_mode_w: wgpu::AddressMode::ClampToEdge,
88        });
89
90        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
91            label: Some("texture-normalize-pipeline-layout"),
92            bind_group_layouts: &[Some(&bind_group_layout)],
93            immediate_size: 0,
94        });
95
96        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
97            label: Some("texture-normalize-pipeline"),
98            layout: Some(&pipeline_layout),
99            vertex: wgpu::VertexState {
100                module: &vertex_shader,
101                entry_point: Some("vs_main"),
102                buffers: &[],
103                compilation_options: wgpu::PipelineCompilationOptions::default(),
104            },
105            fragment: Some(wgpu::FragmentState {
106                module: &fragment_shader,
107                entry_point: Some("fs_main"),
108                targets: &[Some(wgpu::ColorTargetState {
109                    format: wgpu::TextureFormat::Rgba8Unorm,
110                    blend: None,
111                    write_mask: wgpu::ColorWrites::ALL,
112                })],
113                compilation_options: wgpu::PipelineCompilationOptions::default(),
114            }),
115            primitive: wgpu::PrimitiveState::default(),
116            depth_stencil: None,
117            multisample: wgpu::MultisampleState::default(),
118            multiview_mask: None,
119            cache: None,
120        });
121
122        Self {
123            bind_group_layout,
124            render_pipeline,
125            sampler,
126        }
127    }
128
129    /// Normalize `source_texture` into a new Rgba8Unorm texture with top-left origin.
130    ///
131    /// The source texture is sampled with a Y-flip, so bottom-left-origin textures
132    /// (common from GL/Metal) are corrected automatically.
133    pub fn normalize(
134        &self,
135        device: &wgpu::Device,
136        queue: &wgpu::Queue,
137        source_texture: &wgpu::Texture,
138        size: PhysicalSize<u32>,
139    ) -> wgpu::Texture {
140        let output = device.create_texture(&wgpu::TextureDescriptor {
141            label: Some("normalized-imported-frame"),
142            size: wgpu::Extent3d {
143                width: size.width,
144                height: size.height,
145                depth_or_array_layers: 1,
146            },
147            mip_level_count: 1,
148            sample_count: 1,
149            dimension: wgpu::TextureDimension::D2,
150            format: wgpu::TextureFormat::Rgba8Unorm,
151            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
152            view_formats: &[],
153        });
154
155        let source_view = source_texture.create_view(&wgpu::TextureViewDescriptor::default());
156        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
157            label: Some("texture-normalize-bind-group"),
158            layout: &self.bind_group_layout,
159            entries: &[
160                wgpu::BindGroupEntry {
161                    binding: 0,
162                    resource: wgpu::BindingResource::TextureView(&source_view),
163                },
164                wgpu::BindGroupEntry {
165                    binding: 1,
166                    resource: wgpu::BindingResource::Sampler(&self.sampler),
167                },
168            ],
169        });
170
171        let target_view = output.create_view(&wgpu::TextureViewDescriptor::default());
172        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
173            label: Some("texture-normalize-encoder"),
174        });
175
176        {
177            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
178                label: Some("texture-normalize-pass"),
179                timestamp_writes: None,
180                occlusion_query_set: None,
181                depth_stencil_attachment: None,
182                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
183                    view: &target_view,
184                    depth_slice: None,
185                    resolve_target: None,
186                    ops: wgpu::Operations {
187                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
188                        store: wgpu::StoreOp::Store,
189                    },
190                })],
191                multiview_mask: None,
192            });
193            pass.set_pipeline(&self.render_pipeline);
194            pass.set_bind_group(0, &bind_group, &[]);
195            pass.draw(0..3, 0..1);
196        }
197
198        queue.submit(std::iter::once(encoder.finish()));
199        output
200    }
201}