Files
xenia-rs/crates/xenia-ui/src/xenos_pipeline.rs
MechaCat02 3559c8f6ef [iterate-4A] intro-video ROOT #3: render the YUV movie in correct color
The intro video (ADV.wmv) now plays end-to-end in correct color. Three
stacked host-render-path bugs, each masked by the prior:

#3a Multi-texture render path. The host bound a single texture slot, so
the YUV pixel shader's three plane fetches (Y 1280x720 + U/V 640x360, all
k_8) collapsed onto one texture. Expanded the Xenos pipeline to 8 tex+1
sampler slots (xenos_pipeline.rs, xenos_interp.wgsl, translator.rs
headers); each tfetch selects its texture by fetch-constant slot; the
DrawCapture textures tuple now carries the slot; render.rs uploads+binds
every plane per-draw. Also added the scalar-constant ALU ops MULSC/ADDSC/
SUBSC (42-47) the YUV->RGB shader uses.

#3b tfetch destination swizzle. decode_fetch read the tfetch dest as a
4-bit write mask (w1 & 0xF), but Xenos tfetch dword1[0:11] is a 12-bit
destination swizzle (3 bits/component: 0-3=xyzw, 4/5=const 0/1, 6/7=keep).
The result: all three plane fetches did a full-vec4 overwrite of the dest
register, so only the last plane survived. Decode the real 12-bit swizzle
(dest_swizzle) and emit per-lane writes so Y/U/V coexist in r1.x/.y/.z.

#3c Pixel-shader constant bank. Xenos splits the 512-entry float-constant
file: the vertex shader addresses c0..255 -> physical 0..255, but the
pixel shader's c0..255 map to physical 256..511. The game uploads the
YUV->RGB coefficients to physical 510/511. Our translator indexed the low
half for PS constants, reading all-zero -> R=B=Y^2, G=0 (magenta). emit_alu
now adds a const_base of 256 for pixel-stage constant reads.

Plus a bounded (FIFO, 64-entry) host texture cache: the movie streams ~3
new-VA planes per frame, and the previously-unbounded cache exhausted GPU
memory into a device-lost crash mid-playback.

Verified visually: the SQUARE ENIX logo and ADV.wmv footage render in
correct color (was magenta); the translated movie shader now reads
alu[510]/alu[511]; frame green channel is nonzero and R != B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:23:56 +02:00

899 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Host pipeline that consumes PM4 `DRAW_INDX*` captures.
//!
//! Drives [`xenia_gpu::shaders::XENOS_INTERP_WGSL`]. This file owns the
//! wgpu bind-group + pipeline + buffer surface the Xenos WGSL interpreter
//! binds to. The WGSL module is expected to declare:
//!
//! ```text
//! @group(0) @binding(0) var<uniform> xenos_draw : XenosDrawConstants; // 16 B
//! @group(0) @binding(1) var<uniform> xenos_consts : XenosConstants; // ~9.2 KB
//! @group(0) @binding(2) var<storage, read> vs_ucode : array<u32>;
//! @group(0) @binding(3) var<storage, read> ps_ucode : array<u32>;
//! @group(0) @binding(4) var<storage, read> vertex_buffer : array<u32>;
//! ```
//!
//! Texture bindings (M6) are a single-slot stub for P3b:
//!
//! ```text
//! @group(1) @binding(0) var xenos_tex : texture_2d<f32>;
//! @group(1) @binding(1) var xenos_samp : sampler;
//! ```
//!
//! The bound texture is a 1×1 magenta placeholder. Real per-slot guest
//! texture uploads + format decode land with the texture cache (P5).
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;
use xenia_gpu::shaders::XENOS_INTERP_WGSL;
use xenia_gpu::xenos_constants::XenosConstantsBlock;
/// Per-draw constants mirroring the WGSL `XenosDrawConstants` uniform
/// block. Ordering / padding matches `xenos_interp.wgsl` exactly.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct DrawConstants {
draw_index: u32,
vertex_count: u32,
prim_kind: u32,
/// iterate-3O: guest dword base of the uploaded `vertex_buffer` window.
/// The WGSL subtracts this from the absolute vertex-fetch address.
vertex_base_dwords: u32,
/// iterate-3S: guest→host NDC XY transform (mirrors canary
/// `GetHostViewportInfo`). `clip.xy = pos.xy * ndc_scale + ndc_offset*pos.w`.
/// Y is pre-flipped for wgpu. 16 bytes so the block stays 16-byte aligned.
ndc_scale: [f32; 2],
ndc_offset: [f32; 2],
}
/// iterate-3Y: the per-draw host color/blend/write-mask render state, decoded
/// from the guest registers (`RB_BLENDCONTROL0` / `RB_COLOR_MASK`). Used both
/// as part of the pipeline-cache key and to build the `wgpu::ColorTargetState`.
/// Mirrors canary's `GetColorBlendStateForRenderTarget` (D3D12
/// `pipeline_cache.cc`): the factors come straight from `RB_BLENDCONTROL`,
/// and a zero write-mask forces the no-blend `One,Zero` equation.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct RenderState {
/// `RB_BLENDCONTROL0` raw value (RT0). `0x00010001` (One,Zero / One,Zero,
/// Add) is the opaque case.
pub blend_control: u32,
/// RT0 nibble of `RB_COLOR_MASK` (bit0=R … bit3=A). 0 = write nothing.
pub color_mask: u8,
}
impl RenderState {
/// Fully-opaque, all-channels state (the legacy fixed behaviour). Used for
/// procedural/synthetic draws that have no captured guest state.
pub const OPAQUE: RenderState = RenderState {
blend_control: 0x0001_0001,
color_mask: 0xF,
};
/// Map a Xenos `BlendFactor` (5-bit field) to a wgpu `BlendFactor`,
/// mirroring canary `kBlendFactorMap` (D3D12 `pipeline_cache.cc:1504`).
fn map_factor(f: u32) -> wgpu::BlendFactor {
match f {
0 => wgpu::BlendFactor::Zero,
1 => wgpu::BlendFactor::One,
4 => wgpu::BlendFactor::Src,
5 => wgpu::BlendFactor::OneMinusSrc,
6 => wgpu::BlendFactor::SrcAlpha,
7 => wgpu::BlendFactor::OneMinusSrcAlpha,
8 => wgpu::BlendFactor::Dst,
9 => wgpu::BlendFactor::OneMinusDst,
10 => wgpu::BlendFactor::DstAlpha,
11 => wgpu::BlendFactor::OneMinusDstAlpha,
12 => wgpu::BlendFactor::Constant,
13 => wgpu::BlendFactor::OneMinusConstant,
14 => wgpu::BlendFactor::Constant,
15 => wgpu::BlendFactor::OneMinusConstant,
16 => wgpu::BlendFactor::SrcAlphaSaturated,
// 2/3 and >16 are undefined on Xenos; canary maps to Zero.
_ => wgpu::BlendFactor::Zero,
}
}
/// Map a Xenos `BlendFactor` for the *alpha* channel, mirroring canary
/// `kBlendFactorAlphaMap` (color-mode factors collapse to alpha).
fn map_factor_alpha(f: u32) -> wgpu::BlendFactor {
match f {
4 => wgpu::BlendFactor::SrcAlpha,
5 => wgpu::BlendFactor::OneMinusSrcAlpha,
8 => wgpu::BlendFactor::DstAlpha,
9 => wgpu::BlendFactor::OneMinusDstAlpha,
other => Self::map_factor(other),
}
}
fn map_op(o: u32) -> wgpu::BlendOperation {
match o {
0 => wgpu::BlendOperation::Add,
1 => wgpu::BlendOperation::Subtract,
2 => wgpu::BlendOperation::Min,
3 => wgpu::BlendOperation::Max,
4 => wgpu::BlendOperation::ReverseSubtract,
_ => wgpu::BlendOperation::Add,
}
}
/// Build the `wgpu::ColorTargetState` for this draw.
fn color_target(&self, format: wgpu::TextureFormat) -> wgpu::ColorTargetState {
let bc = self.blend_control;
let color_src = bc & 0x1F;
let color_op = (bc >> 5) & 0x7;
let color_dst = (bc >> 8) & 0x1F;
let alpha_src = (bc >> 16) & 0x1F;
let alpha_op = (bc >> 21) & 0x7;
let alpha_dst = (bc >> 24) & 0x1F;
// wgpu requires `blend: None` when nothing would be written; also the
// `One,Zero,Add` identity is the opaque case (canary's no-blend), which
// we express as `blend: None` so it's a plain overwrite.
let is_opaque = color_src == 1
&& color_dst == 0
&& color_op == 0
&& alpha_src == 1
&& alpha_dst == 0
&& alpha_op == 0;
let blend = if is_opaque {
None
} else {
Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: Self::map_factor(color_src),
dst_factor: Self::map_factor(color_dst),
operation: Self::map_op(color_op),
},
alpha: wgpu::BlendComponent {
src_factor: Self::map_factor_alpha(alpha_src),
dst_factor: Self::map_factor_alpha(alpha_dst),
operation: Self::map_op(alpha_op),
},
})
};
let mut write_mask = wgpu::ColorWrites::empty();
if self.color_mask & 0x1 != 0 {
write_mask |= wgpu::ColorWrites::RED;
}
if self.color_mask & 0x2 != 0 {
write_mask |= wgpu::ColorWrites::GREEN;
}
if self.color_mask & 0x4 != 0 {
write_mask |= wgpu::ColorWrites::BLUE;
}
if self.color_mask & 0x8 != 0 {
write_mask |= wgpu::ColorWrites::ALPHA;
}
wgpu::ColorTargetState {
format,
blend,
write_mask,
}
}
}
/// Submitted to [`XenosPipeline::render_one`] to render one captured draw.
#[derive(Clone, Copy, Debug)]
pub struct DrawRequest {
/// Monotonic draw counter; shader uses it for per-draw colour rotation.
pub draw_index: u32,
/// Host-normalised vertex count (after primitive-processor rewrite).
pub vertex_count: u32,
/// Xenos primitive-type code; shader may branch on it in P3b+.
pub prim_kind: u32,
/// iterate-3O: guest dword base of the per-draw vertex window uploaded to
/// `vertex_buffer` (b4). 0 = no real vertex window (procedural fallback).
pub vertex_base_dwords: u32,
/// iterate-3S: guest→host NDC XY transform (Y pre-flipped). When all-zero
/// the shader leaves the position untransformed (procedural fallback).
pub ndc_scale: [f32; 2],
pub ndc_offset: [f32; 2],
}
/// Reasonable upper bound on a single shader blob (dwords). Most Xbox 360
/// shaders are ≪ 4 KB; 64 KB is orders-of-magnitude slack.
const UCODE_BUFFER_MAX_DWORDS: u64 = 16 * 1024; // 64 KB each for VS & PS
/// 16 MB of vertex data — enough for any realistic Xenos draw.
const VERTEX_BUFFER_MAX_BYTES: u64 = 16 * 1024 * 1024;
pub struct XenosPipeline {
/// Interpreter pipeline with the legacy fixed (alpha-blend) state. Kept as
/// the default; per-state variants are built lazily in `interp_cache`.
pipeline: wgpu::RenderPipeline,
/// iterate-3Y: the interpreter WGSL module, retained so per-render-state
/// interpreter pipelines can be compiled on demand.
interp_shader: wgpu::ShaderModule,
/// iterate-3Y: interpreter pipelines keyed on the per-draw `RenderState`
/// (blend + write mask), so flat/alpha/opaque draws composite correctly
/// even when their (vs,ps) didn't translate.
interp_cache: std::collections::HashMap<RenderState, wgpu::RenderPipeline>,
draw_ctx_buffer: wgpu::Buffer,
constants_buffer: wgpu::Buffer,
vs_ucode_buffer: wgpu::Buffer,
ps_ucode_buffer: wgpu::Buffer,
vertex_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
/// P5: swapped per-draw when a new cached texture becomes active.
tex_bind_group: wgpu::BindGroup,
/// Layout + sampler retained so `set_texture_view` can rebuild
/// `tex_bind_group` on the fly without re-reading the pipeline.
tex_bgl: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
/// Fallback 1×1 magenta texture — used when no guest texture has been
/// uploaded yet or when a draw references an unsupported format.
dummy_view: wgpu::TextureView,
/// P7 — retained pipeline layout + compiled-pipeline cache for
/// Xenos→WGSL translator output. Keyed on `(vs_blob_key, ps_blob_key)`
/// so every (vs, ps) pair gets compiled once and re-used for every
/// subsequent draw. Interpreter pipeline remains the fallback.
pipeline_layout: wgpu::PipelineLayout,
/// iterate-3Y: cached translator pipelines keyed on the shader pair AND the
/// per-draw render state, so the same (vs,ps) with different blend/mask
/// composites correctly. The translated WGSL module is itself cached per
/// (vs,ps) so re-translation only happens once.
translated_cache: std::collections::HashMap<(u32, u32, RenderState), wgpu::RenderPipeline>,
translated_modules: std::collections::HashMap<(u32, u32), wgpu::ShaderModule>,
pub target_format: wgpu::TextureFormat,
}
/// Number of simultaneously-bindable Xenos texture slots in `group(1)`
/// (binding 0 = shared sampler, bindings 1..=TEX_SLOTS = tex0..). Must match
/// the binding count in `xenos_interp.wgsl`. 8 covers the intro video's 3
/// YUV planes with headroom while staying well under the 16-sampled-texture
/// per-stage limit.
pub const TEX_SLOTS: usize = 8;
impl XenosPipeline {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
target_format: wgpu::TextureFormat,
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("xenos_interp.wgsl"),
source: wgpu::ShaderSource::Wgsl(XENOS_INTERP_WGSL.into()),
});
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("xenos bind group layout"),
entries: &[
// b0: draw_ctx (16 B uniform)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: std::num::NonZeroU64::new(
std::mem::size_of::<DrawConstants>() as u64,
),
},
count: None,
},
// b1: XenosConstants read-only storage (~9.2 KB). Not uniform
// because the block contains packed `array<u32>` fields and
// WGSL's uniform address space would require 16-byte stride.
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: std::num::NonZeroU64::new(
XenosConstantsBlock::SIZE as u64,
),
},
count: None,
},
// b2: vs_ucode (read-only storage)
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
// b3: ps_ucode (read-only storage)
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
// b4: vertex_buffer (read-only storage)
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let mut tex_bgl_entries: Vec<wgpu::BindGroupLayoutEntry> =
Vec::with_capacity(TEX_SLOTS + 1);
tex_bgl_entries.push(wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
});
for k in 0..TEX_SLOTS {
tex_bgl_entries.push(wgpu::BindGroupLayoutEntry {
binding: (k + 1) as u32,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
});
}
let tex_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("xenos tex bind group layout"),
entries: &tex_bgl_entries,
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("xenos pipeline layout"),
bind_group_layouts: &[&bgl, &tex_bgl],
push_constant_ranges: &[],
});
// Buffer allocation. `queue.write_buffer` uses COPY_DST; all
// interpreter-facing buffers need it.
let initial_draw = DrawConstants {
draw_index: 0,
vertex_count: 3,
prim_kind: 4,
vertex_base_dwords: 0,
ndc_scale: [0.0, 0.0],
ndc_offset: [0.0, 0.0],
};
let draw_ctx_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("xenos draw ctx"),
contents: bytemuck::bytes_of(&initial_draw),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("xenos constants"),
size: XenosConstantsBlock::SIZE as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let vs_ucode_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("xenos vs ucode"),
size: UCODE_BUFFER_MAX_DWORDS * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let ps_ucode_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("xenos ps ucode"),
size: UCODE_BUFFER_MAX_DWORDS * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("xenos vertex buffer"),
size: VERTEX_BUFFER_MAX_BYTES,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Dummy 1×1 magenta texture — placeholder until P5's texture cache
// lands. Every `interpret_texture_fetch` samples this for now so the
// interpreter can exercise textureSample paths without a real cache.
let dummy_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("xenos dummy texture"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
// iterate-3Y: transparent black (0,0,0,0). When a textured draw's
// real texture can't be resolved (e.g. its sampler slot is shadowed by
// a vertex-fetch constant), sampling a *transparent* texel makes the
// draw a no-op under its real premultiplied-alpha blend — instead of
// fabricating an opaque magenta that overpaints everything (the old
// debug stub). This removes a fake rather than adding one: we never
// invent visible pixels for an unresolved texture.
queue.write_texture(
wgpu::ImageCopyTexture {
texture: &dummy_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&[0x00u8, 0x00, 0x00, 0x00],
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
let dummy_view = dummy_tex.create_view(&wgpu::TextureViewDescriptor::default());
let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("xenos dummy sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let mut tex_bg_entries: Vec<wgpu::BindGroupEntry> = Vec::with_capacity(TEX_SLOTS + 1);
tex_bg_entries.push(wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&dummy_sampler),
});
for k in 0..TEX_SLOTS {
tex_bg_entries.push(wgpu::BindGroupEntry {
binding: (k + 1) as u32,
resource: wgpu::BindingResource::TextureView(&dummy_view),
});
}
let tex_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("xenos tex bind group"),
layout: &tex_bgl,
entries: &tex_bg_entries,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("xenos bind group"),
layout: &bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: draw_ctx_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: constants_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vs_ucode_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: ps_ucode_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: vertex_buffer.as_entire_binding(),
},
],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("xenos_interp pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: target_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::OVER,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
pipeline,
interp_shader: shader,
interp_cache: std::collections::HashMap::new(),
draw_ctx_buffer,
constants_buffer,
vs_ucode_buffer,
ps_ucode_buffer,
vertex_buffer,
bind_group,
tex_bind_group,
tex_bgl,
sampler: dummy_sampler,
dummy_view,
pipeline_layout: layout,
translated_cache: std::collections::HashMap::new(),
translated_modules: std::collections::HashMap::new(),
target_format,
}
}
/// P7 — has the translator already produced a WGSL *module* for this
/// (vs, ps) pair? (A per-render-state pipeline may still need building.)
pub fn has_translated(&self, vs_blob_key: u32, ps_blob_key: u32) -> bool {
self.translated_modules
.contains_key(&(vs_blob_key, ps_blob_key))
}
/// P7 — compile a translator-produced WGSL module and cache it keyed on
/// `(vs_blob_key, ps_blob_key)`. The actual `RenderPipeline` (which also
/// depends on the per-draw blend/mask state) is built lazily by
/// [`render_one_translated`]. Returns `true` on success.
pub fn insert_translated(
&mut self,
device: &wgpu::Device,
vs_blob_key: u32,
ps_blob_key: u32,
wgsl: &str,
) -> bool {
let key = (vs_blob_key, ps_blob_key);
if self.translated_modules.contains_key(&key) {
return true;
}
let shader = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("xenos translated module"),
source: wgpu::ShaderSource::Wgsl(wgsl.to_string().into()),
})
})) {
Ok(m) => m,
Err(_) => {
metrics::counter!("gpu.shader.compile_err", "stage" => "module")
.increment(1);
return false;
}
};
self.translated_modules.insert(key, shader);
metrics::counter!("gpu.shader.compile_ok").increment(1);
true
}
/// iterate-3Y: ensure a translator pipeline exists for `(vs,ps,rstate)`,
/// building it from the cached module + the per-draw color/blend target.
fn ensure_translated_for_state(
&mut self,
device: &wgpu::Device,
vs_key: u32,
ps_key: u32,
rstate: RenderState,
) -> bool {
let pkey = (vs_key, ps_key, rstate);
if self.translated_cache.contains_key(&pkey) {
return true;
}
let Some(module) = self.translated_modules.get(&(vs_key, ps_key)) else {
return false;
};
let target = rstate.color_target(self.target_format);
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("xenos translated pipeline"),
layout: Some(&self.pipeline_layout),
vertex: wgpu::VertexState {
module,
entry_point: "vs_main",
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module,
entry_point: "fs_main",
compilation_options: Default::default(),
targets: &[Some(target)],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
self.translated_cache.insert(pkey, pipeline);
true
}
/// iterate-3Y: ensure an interpreter pipeline exists for `rstate`.
fn ensure_interp_for_state(&mut self, device: &wgpu::Device, rstate: RenderState) {
if self.interp_cache.contains_key(&rstate) {
return;
}
let target = rstate.color_target(self.target_format);
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("xenos interp pipeline (per-state)"),
layout: Some(&self.pipeline_layout),
vertex: wgpu::VertexState {
module: &self.interp_shader,
entry_point: "vs_main",
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &self.interp_shader,
entry_point: "fs_main",
compilation_options: Default::default(),
targets: &[Some(target)],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
self.interp_cache.insert(rstate, pipeline);
}
/// iterate-3Y: render one draw through the translator pipeline built for
/// this draw's render state. Returns `false` if no module is cached for
/// `(vs,ps)` (caller should fall back to the interpreter).
pub fn render_one_translated(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
req: DrawRequest,
vs_key: u32,
ps_key: u32,
rstate: RenderState,
) -> bool {
if !self.ensure_translated_for_state(device, vs_key, ps_key, rstate) {
return false;
}
let cb = DrawConstants {
draw_index: req.draw_index,
vertex_count: req.vertex_count.max(3),
prim_kind: req.prim_kind,
vertex_base_dwords: req.vertex_base_dwords,
ndc_scale: req.ndc_scale,
ndc_offset: req.ndc_offset,
};
queue.write_buffer(&self.draw_ctx_buffer, 0, bytemuck::bytes_of(&cb));
let pipeline = self
.translated_cache
.get(&(vs_key, ps_key, rstate))
.expect("just ensured");
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("xenos translated draw"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.set_bind_group(1, &self.tex_bind_group, &[]);
let rounded = req.vertex_count.div_ceil(3) * 3;
pass.draw(0..rounded.max(3), 0..1);
true
}
/// Number of distinct translator pipelines cached. Surfaced to the HUD.
pub fn translated_pipeline_count(&self) -> usize {
self.translated_cache.len()
}
/// P5 — swap the active texture bound at `@group(1) @binding(0)`.
/// `view` is typically a wgpu texture view obtained from the
/// [`TextureCacheHost`]. Pass `None` to revert to the built-in dummy
/// magenta stub.
pub fn set_texture_view(&mut self, device: &wgpu::Device, view: Option<&wgpu::TextureView>) {
self.set_texture_slots(device, &[view]);
}
/// Rebind `group(1)` with per-slot texture views. `views[k]` binds to
/// texture slot `k` (the `tfetch` fetch-constant index the shader selects);
/// `None`, or any slot past `views.len()`/`TEX_SLOTS`, falls back to the
/// transparent dummy. The shared sampler stays at binding 0. Each call
/// rebuilds the bind group, so per-draw slot sets composite correctly.
pub fn set_texture_slots(
&mut self,
device: &wgpu::Device,
views: &[Option<&wgpu::TextureView>],
) {
let mut entries: Vec<wgpu::BindGroupEntry> = Vec::with_capacity(TEX_SLOTS + 1);
entries.push(wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&self.sampler),
});
for k in 0..TEX_SLOTS {
let v = views.get(k).copied().flatten().unwrap_or(&self.dummy_view);
entries.push(wgpu::BindGroupEntry {
binding: (k + 1) as u32,
resource: wgpu::BindingResource::TextureView(v),
});
}
self.tex_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("xenos tex bind group (slots)"),
layout: &self.tex_bgl,
entries: &entries,
});
}
/// Clear `target_view` to `color`, store.
pub fn clear(
&self,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
color: [f64; 4],
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("xenos frontbuffer clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: color[0],
g: color[1],
b: color[2],
a: color[3],
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
let _ = &mut pass;
}
/// Upload shader microcode + constants once (before the batch of draws
/// that share them). Skips zero-length blobs.
pub fn upload_shader_and_constants(
&self,
queue: &wgpu::Queue,
vs_ucode: &[u32],
ps_ucode: &[u32],
constants: &XenosConstantsBlock,
) {
queue.write_buffer(&self.constants_buffer, 0, bytemuck::bytes_of(constants));
if !vs_ucode.is_empty() {
let bytes: &[u8] = bytemuck::cast_slice(vs_ucode);
let max = (UCODE_BUFFER_MAX_DWORDS * 4) as usize;
queue.write_buffer(&self.vs_ucode_buffer, 0, &bytes[..bytes.len().min(max)]);
}
if !ps_ucode.is_empty() {
let bytes: &[u8] = bytemuck::cast_slice(ps_ucode);
let max = (UCODE_BUFFER_MAX_DWORDS * 4) as usize;
queue.write_buffer(&self.ps_ucode_buffer, 0, &bytes[..bytes.len().min(max)]);
}
}
/// Upload vertex data (as raw big-endian dwords — the WGSL side will
/// bswap as needed during format unpacking).
pub fn upload_vertex_data(&self, queue: &wgpu::Queue, data: &[u32]) {
if data.is_empty() {
return;
}
let bytes: &[u8] = bytemuck::cast_slice(data);
let max = VERTEX_BUFFER_MAX_BYTES as usize;
queue.write_buffer(&self.vertex_buffer, 0, &bytes[..bytes.len().min(max)]);
}
/// Render one captured draw through the interpreter, using the per-draw
/// `rstate` (blend/write-mask) so flat draws composite correctly even
/// when their (vs,ps) didn't translate. `RenderState::OPAQUE` reproduces
/// the legacy fixed behaviour for procedural/synthetic draws.
pub fn render_one(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
req: DrawRequest,
rstate: RenderState,
) {
self.ensure_interp_for_state(device, rstate);
let cb = DrawConstants {
draw_index: req.draw_index,
vertex_count: req.vertex_count.max(3),
prim_kind: req.prim_kind,
vertex_base_dwords: req.vertex_base_dwords,
ndc_scale: req.ndc_scale,
ndc_offset: req.ndc_offset,
};
queue.write_buffer(&self.draw_ctx_buffer, 0, bytemuck::bytes_of(&cb));
let pipeline = self
.interp_cache
.get(&rstate)
.expect("just ensured");
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("xenos draw"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.set_bind_group(1, &self.tex_bind_group, &[]);
let rounded = req.vertex_count.div_ceil(3) * 3;
pass.draw(0..rounded.max(3), 0..1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draw_constants_layout_matches_wgsl_uniform() {
assert_eq!(std::mem::size_of::<DrawConstants>(), 32);
}
}