#include "tex_copy_conv.hpp" #include "tex_copy_format_contract.hpp" #include "../internal.hpp" #include "../gx/gx.hpp" #include "../webgpu/gpu.hpp" #include "texture.hpp" #include "../gx/gx_fmt.hpp" #include #include "texture_convert.hpp" using namespace std::string_literals; namespace aurora::gfx::tex_copy_conv { static Module Log("aurora::gfx::tex_copy_conv"); using webgpu::g_device; static constexpr std::string_view ShaderPreamble = R"( @group(0) @binding(0) var src_samp: sampler; @group(0) @binding(1) var src: texture_2d; struct UVTransform { offset: vec2f, scale: vec2f, copy_filter: vec4f, flags: vec4f, }; @group(0) @binding(2) var uv_xf: UVTransform; struct VertexOutput { @builtin(position) pos: vec4f, @location(0) uv: vec2f, }; var positions: array = array( vec2f(-1.0, 1.0), vec2f(-1.0, -3.0), vec2f(3.0, 1.0), ); var uvs: array = array( vec2f(0.0, 0.0), vec2f(0.0, 2.0), vec2f(2.0, 0.0), ); @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { var out: VertexOutput; out.pos = vec4f(positions[vi], 0.0, 1.0); out.uv = uvs[vi] * uv_xf.scale + uv_xf.offset; return out; } fn intensity(rgb: vec3f) -> f32 { // ITU-R BT.601 luma coefficients return dot(rgb, vec3f(0.257, 0.504, 0.098)) + 16.0 / 255.0; } fn quantize4(v: f32) -> f32 { return floor(v * 16.0) / 15.0; } fn apply_opaque_alpha(c: vec4f) -> vec4f { if (uv_xf.flags.x != 0.0) { return vec4f(c.rgb, 1.0); } return c; } fn clamp_copy_uv(uv: vec2f) -> vec2f { return vec2f(uv.x, clamp(uv.y, uv_xf.flags.z, uv_xf.flags.w)); } fn sample_copy(uv: vec2f) -> vec4f { let current = textureSample(src, src_samp, clamp_copy_uv(uv)); if (uv_xf.copy_filter.w == 0.0) { return apply_opaque_alpha(current); } let tex_size = vec2f(textureDimensions(src)); let pixel_size = vec2f(1.0, 1.0) / tex_size; let row_stride = max(uv_xf.flags.y, 1.0); let prev = textureSample(src, src_samp, clamp_copy_uv(uv - vec2f(0.0, pixel_size.y * row_stride))); let next = textureSample(src, src_samp, clamp_copy_uv(uv + vec2f(0.0, pixel_size.y * row_stride))); let prev_rgb = floor(prev.rgb * 255.0 + vec3f(0.5)); let current_rgb = floor(current.rgb * 255.0 + vec3f(0.5)); let next_rgb = floor(next.rgb * 255.0 + vec3f(0.5)); let filtered_rgb = min( floor((prev_rgb * uv_xf.copy_filter.x + current_rgb * uv_xf.copy_filter.y + next_rgb * uv_xf.copy_filter.z) / 64.0), vec3f(255.0)); let filtered = vec4f(filtered_rgb / 255.0, current.a); return apply_opaque_alpha(filtered); } )"sv; static const std::string DepthShaderPreamble = R"( @group(0) @binding(0) var src: texture_depth_2d; struct UVTransform { offset: vec2f, scale: vec2f, copy_filter: vec4f, flags: vec4f, }; @group(0) @binding(1) var uv_xf: UVTransform; struct VertexOutput { @builtin(position) pos: vec4f, @location(0) uv: vec2f, }; var positions: array = array( vec2f(-1.0, 1.0), vec2f(-1.0, -3.0), vec2f(3.0, 1.0), ); var uvs: array = array( vec2f(0.0, 0.0), vec2f(0.0, 2.0), vec2f(2.0, 0.0), ); @vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { var out: VertexOutput; out.pos = vec4f(positions[vi], 0.0, 1.0); out.uv = uvs[vi] * uv_xf.scale + uv_xf.offset; return out; } )"s + (gx::UseReversedZ ? R"( fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 { let tex_size = vec2i(textureDimensions(src)); let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); let depth = textureLoad(src, coord, 0); return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); } )"s : R"( fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 { let tex_size = vec2i(textureDimensions(src)); let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); let depth = textureLoad(src, coord, 0); return min(u32(clamp(depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu); } )"s) + R"( fn gx_depth_bytes(z24: u32) -> vec3u { return vec3u((z24 >> 16u) & 0xffu, (z24 >> 8u) & 0xffu, z24 & 0xffu); } fn clamp_copy_coord(coord: vec2i, tex_size: vec2i) -> vec2i { let top = i32(floor(uv_xf.flags.z * f32(tex_size.y))); let bottom = i32(floor(uv_xf.flags.w * f32(tex_size.y))); return vec2i(coord.x, clamp(coord.y, top, bottom)); } fn sample_depth_copy(uv: vec2f) -> vec4u { let tex_size = vec2i(textureDimensions(src)); let current_coord = clamp_copy_coord(vec2i(floor(uv * vec2f(tex_size))), tex_size); let current = gx_depth_bytes(gx_z24_at_coord(current_coord)); if (uv_xf.copy_filter.w == 0.0) { return vec4u(current, 255u); } // GX applies its vertical copy filter to depth copies too, filtering the high/middle/low Z bytes // independently before the destination format picks bytes. Alpha is unfiltered and 255. let row_stride = max(i32(round(uv_xf.flags.y)), 1); let prev = gx_depth_bytes(gx_z24_at_coord( clamp_copy_coord(current_coord - vec2i(0, row_stride), tex_size))); let next = gx_depth_bytes(gx_z24_at_coord( clamp_copy_coord(current_coord + vec2i(0, row_stride), tex_size))); let coefficients = vec3u(uv_xf.copy_filter.xyz); let combined = prev * coefficients.x + current * coefficients.y + next * coefficients.z; var filtered = combined >> vec3u(6u); // The copy-filter accumulator wraps to nine bits when coefficients can // produce values at or above 512, before saturating to an eight-bit byte. if (coefficients.x + coefficients.y + coefficients.z >= 128u) { filtered = filtered & vec3u(0x1ffu); } filtered = min(filtered, vec3u(255u)); return vec4u(filtered, 255u); } )"s; // Passthrough blit (for scaling) static constexpr std::string_view FragPassthrough = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { return sample_copy(in.uv); } )"sv; // GX_TF_I4: 4-bit intensity -> R8Unorm (quantized) static constexpr std::string_view FragI4 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let rgb = sample_copy(in.uv).rgb; let i = quantize4(intensity(rgb)); return vec4f(i, i, i, i); } )"sv; // GX_TF_I8: 8-bit intensity -> R8Unorm static constexpr std::string_view FragI8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let rgb = sample_copy(in.uv).rgb; let i = intensity(rgb); return vec4f(i, i, i, i); } )"sv; // GX_TF_IA4: 4-bit intensity + 4-bit alpha -> RG8Unorm static constexpr std::string_view FragIA4 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); let i = quantize4(intensity(c.rgb)); let a = quantize4(c.a); return vec4f(i, i, i, a); } )"sv; // GX_TF_IA8: 8-bit intensity + 8-bit alpha -> RG8Unorm static constexpr std::string_view FragIA8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); let i = intensity(c.rgb); return vec4f(i, i, i, c.a); } )"sv; // GX_TF_RGB565: Blit alpha to 1.0 static constexpr std::string_view FragRGB565 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); return vec4f(c.rgb, 1.0); } )"sv; // GX_CTF_R4: 4-bit red -> R8Unorm static constexpr std::string_view FragR4 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let r = quantize4(sample_copy(in.uv).r); return vec4f(r, r, r, r); } )"sv; // GX_CTF_RA4: 4-bit red + 4-bit alpha -> RG8Unorm static constexpr std::string_view FragRA4 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); let r = quantize4(c.r); return vec4f(r, r, r, quantize4(c.a)); } )"sv; // GX_CTF_RA8: 8-bit red + 8-bit alpha -> RG8Unorm static constexpr std::string_view FragRA8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); return vec4f(c.r, c.r, c.r, c.a); } )"sv; // GX_CTF_A8: 8-bit alpha -> R8Unorm static constexpr std::string_view FragA8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let a = sample_copy(in.uv).a; return vec4f(a, a, a, a); } )"sv; // GX_CTF_R8: 8-bit red -> R8Unorm static constexpr std::string_view FragR8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let r = sample_copy(in.uv).r; return vec4f(r, r, r, r); } )"sv; // GX_CTF_G8: 8-bit green -> R8Unorm static constexpr std::string_view FragG8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let g = sample_copy(in.uv).g; return vec4f(g, g, g, g); } )"sv; // GX_CTF_B8: 8-bit blue -> R8Unorm static constexpr std::string_view FragB8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let b = sample_copy(in.uv).b; return vec4f(b, b, b, b); } )"sv; // GX_CTF_RG8: 8-bit red + 8-bit green -> RG8Unorm static constexpr std::string_view FragRG8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); return vec4f(c.r, c.r, c.r, c.g); } )"sv; // GX_CTF_GB8: 8-bit green + 8-bit blue -> RG8Unorm static constexpr std::string_view FragGB8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let c = sample_copy(in.uv); return vec4f(c.g, c.g, c.g, c.b); } )"sv; // GX_TF_Z8 stores the high-Z byte in I8 storage, which samples as intensity // replicated across all four channels. static constexpr std::string_view FragZ8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let depth_bytes = sample_depth_copy(in.uv); let z8 = f32(depth_bytes.r) / 255.0; return vec4f(z8, z8, z8, z8); } )"sv; // GX_TF_Z16 is the depth RA8 copy path: depth samples have opaque alpha and their high-Z byte in // red, so the encoded [A, R] pair samples through IA8 as high-Z intensity with opaque alpha. static constexpr std::string_view FragZ16 = detail::Z16FragmentShader; // GX_TF_Z24X8 uses the RGBA8 storage shape for copies, with the 24-bit depth // value available to z-texture sampling through RGB. static constexpr std::string_view FragZ24X8 = R"( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let depth_bytes = sample_depth_copy(in.uv); let r = f32(depth_bytes.r) / 255.0; let g = f32(depth_bytes.g) / 255.0; let b = f32(depth_bytes.b) / 255.0; return vec4f(r, g, b, 1.0); } )"sv; struct ConvPipeline { GXTexFmt fmt; std::string_view fragShader; wgpu::TextureFormat outputFormat; const char* label; }; static constexpr std::array ConvPipelines{ ConvPipeline{GX_TF_I4, FragI4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv I4"}, ConvPipeline{GX_TF_I8, FragI8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv I8"}, ConvPipeline{GX_TF_IA4, FragIA4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv IA4"}, ConvPipeline{GX_TF_IA8, FragIA8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv IA8"}, ConvPipeline{GX_TF_RGB565, FragRGB565, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RGB565"}, ConvPipeline{GX_CTF_R4, FragR4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv R4"}, ConvPipeline{GX_CTF_RA4, FragRA4, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RA4"}, ConvPipeline{GX_CTF_RA8, FragRA8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RA8"}, ConvPipeline{GX_CTF_A8, FragA8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv A8"}, ConvPipeline{GX_CTF_R8, FragR8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv R8"}, ConvPipeline{GX_CTF_G8, FragG8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv G8"}, ConvPipeline{GX_CTF_B8, FragB8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv B8"}, ConvPipeline{GX_CTF_RG8, FragRG8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv RG8"}, ConvPipeline{GX_CTF_GB8, FragGB8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv GB8"}, }; static constexpr std::array DepthConvPipelines{ ConvPipeline{GX_TF_Z8, FragZ8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv Z8"}, ConvPipeline{GX_TF_Z16, FragZ16, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv Z16"}, ConvPipeline{GX_TF_Z24X8, FragZ24X8, wgpu::TextureFormat::RGBA8Unorm, "TexCopyConv Z24X8"}, }; static wgpu::BindGroupLayout g_bindGroupLayout; static wgpu::BindGroupLayout g_depthBindGroupLayout; static wgpu::Sampler g_nearestSampler; static wgpu::Sampler g_linearSampler; static absl::flat_hash_map g_pipelines; static wgpu::RenderPipeline g_blitPipeline; static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble, const wgpu::BindGroupLayout& bindGroupLayout) { std::string shaderSource; shaderSource.reserve(shaderPreamble.size() + conv.fragShader.size()); shaderSource += shaderPreamble; shaderSource += conv.fragShader; const wgpu::ShaderSourceWGSL wgslSource{wgpu::ShaderSourceWGSL::Init{ .code = shaderSource.c_str(), }}; const wgpu::ShaderModuleDescriptor moduleDescriptor{ .nextInChain = &wgslSource, .label = conv.label, }; const auto module = g_device.CreateShaderModule(&moduleDescriptor); const std::array colorTargets{wgpu::ColorTargetState{ .format = conv.outputFormat, }}; const wgpu::FragmentState fragmentState{ .module = module, .entryPoint = "fs_main", .targetCount = colorTargets.size(), .targets = colorTargets.data(), }; const wgpu::PipelineLayoutDescriptor layoutDescriptor{ .bindGroupLayoutCount = 1, .bindGroupLayouts = &bindGroupLayout, }; const auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); const wgpu::RenderPipelineDescriptor pipelineDescriptor{ .label = conv.label, .layout = pipelineLayout, .vertex = wgpu::VertexState{ .module = module, .entryPoint = "vs_main", }, .primitive = wgpu::PrimitiveState{ .topology = wgpu::PrimitiveTopology::TriangleList, }, .fragment = &fragmentState, }; return g_device.CreateRenderPipeline(&pipelineDescriptor); } bool needs_conversion(const GXTexFmt fmt) { return g_pipelines.contains(fmt); } void initialize() { static constexpr std::array bindGroupLayoutEntries{ wgpu::BindGroupLayoutEntry{ .binding = 0, .visibility = wgpu::ShaderStage::Fragment, .sampler = wgpu::SamplerBindingLayout{ .type = wgpu::SamplerBindingType::Filtering, }, }, wgpu::BindGroupLayoutEntry{ .binding = 1, .visibility = wgpu::ShaderStage::Fragment, .texture = wgpu::TextureBindingLayout{ .sampleType = wgpu::TextureSampleType::Float, .viewDimension = wgpu::TextureViewDimension::e2D, }, }, wgpu::BindGroupLayoutEntry{ .binding = 2, .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, .buffer = wgpu::BufferBindingLayout{ .type = wgpu::BufferBindingType::Uniform, }, }, }; static constexpr wgpu::BindGroupLayoutDescriptor bindGroupLayoutDescriptor{ .label = "TexCopyConv Bind Group Layout", .entryCount = bindGroupLayoutEntries.size(), .entries = bindGroupLayoutEntries.data(), }; g_bindGroupLayout = g_device.CreateBindGroupLayout(&bindGroupLayoutDescriptor); static constexpr std::array depthBindGroupLayoutEntries{ wgpu::BindGroupLayoutEntry{ .binding = 0, .visibility = wgpu::ShaderStage::Fragment, .texture = wgpu::TextureBindingLayout{ .sampleType = wgpu::TextureSampleType::Depth, .viewDimension = wgpu::TextureViewDimension::e2D, }, }, wgpu::BindGroupLayoutEntry{ .binding = 1, .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, .buffer = wgpu::BufferBindingLayout{ .type = wgpu::BufferBindingType::Uniform, }, }, }; static constexpr wgpu::BindGroupLayoutDescriptor depthBindGroupLayoutDescriptor{ .label = "TexCopyConv Depth Bind Group Layout", .entryCount = depthBindGroupLayoutEntries.size(), .entries = depthBindGroupLayoutEntries.data(), }; g_depthBindGroupLayout = g_device.CreateBindGroupLayout(&depthBindGroupLayoutDescriptor); g_blitPipeline = create_pipeline( {GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"}, ShaderPreamble, g_bindGroupLayout); for (const auto& conv : ConvPipelines) { g_pipelines[conv.fmt] = create_pipeline(conv, ShaderPreamble, g_bindGroupLayout); if (conv.outputFormat != to_wgpu(conv.fmt)) { Log.fatal("Output format mismatch for {}", conv.fmt); } } for (const auto& conv : DepthConvPipelines) { g_pipelines[conv.fmt] = create_pipeline(conv, DepthShaderPreamble, g_depthBindGroupLayout); if (conv.outputFormat != to_wgpu(conv.fmt)) { Log.fatal("Output format mismatch for {}", conv.fmt); } } static constexpr wgpu::SamplerDescriptor nearestSamplerDescriptor{ .label = "TexCopyConv Nearest Sampler", .magFilter = wgpu::FilterMode::Nearest, .minFilter = wgpu::FilterMode::Nearest, }; g_nearestSampler = g_device.CreateSampler(&nearestSamplerDescriptor); static constexpr wgpu::SamplerDescriptor linearSamplerDescriptor{ .label = "TexCopyConv Linear Sampler", .magFilter = wgpu::FilterMode::Linear, .minFilter = wgpu::FilterMode::Linear, }; g_linearSampler = g_device.CreateSampler(&linearSamplerDescriptor); } void shutdown() { g_pipelines.clear(); g_blitPipeline = {}; g_bindGroupLayout = {}; g_depthBindGroupLayout = {}; g_nearestSampler = {}; g_linearSampler = {}; } static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, const wgpu::RenderPipeline& pipeline) { wgpu::BindGroup bindGroup; if (gx::is_depth_format(req.fmt)) { const std::array bindGroupEntries{ wgpu::BindGroupEntry{ .binding = 0, .textureView = req.srcView, }, wgpu::BindGroupEntry{ .binding = 1, .buffer = g_uniformBuffer, .offset = req.uniformRange.offset, .size = req.uniformRange.size, }, }; const wgpu::BindGroupDescriptor bindGroupDescriptor{ .layout = g_depthBindGroupLayout, .entryCount = bindGroupEntries.size(), .entries = bindGroupEntries.data(), }; bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); } else { const auto& sampler = req.sampleFilter == SampleFilter::Linear ? g_linearSampler : g_nearestSampler; const std::array bindGroupEntries{ wgpu::BindGroupEntry{ .binding = 0, .sampler = sampler, }, wgpu::BindGroupEntry{ .binding = 1, .textureView = req.srcView, }, wgpu::BindGroupEntry{ .binding = 2, .buffer = g_uniformBuffer, .offset = req.uniformRange.offset, .size = req.uniformRange.size, }, }; const wgpu::BindGroupDescriptor bindGroupDescriptor{ .layout = g_bindGroupLayout, .entryCount = bindGroupEntries.size(), .entries = bindGroupEntries.data(), }; bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); } const std::array colorAttachments{ wgpu::RenderPassColorAttachment{ .view = req.dst->attachmentTextureView, .loadOp = wgpu::LoadOp::Clear, .storeOp = wgpu::StoreOp::Store, .clearValue = {0.0, 0.0, 0.0, 0.0}, }, }; const wgpu::RenderPassDescriptor renderPassDescriptor{ .label = "TexCopyConv Pass", .colorAttachmentCount = colorAttachments.size(), .colorAttachments = colorAttachments.data(), }; const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); pass.SetPipeline(pipeline); pass.SetBindGroup(0, bindGroup); pass.Draw(3); pass.End(); } void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { const auto it = g_pipelines.find(req.fmt); if (it == g_pipelines.end()) { Log.fatal("No copy conversion pipeline for format {}", static_cast(req.fmt)); } execute(cmd, req, it->second); } void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); } } // namespace aurora::gfx::tex_copy_conv