[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>
This commit is contained in:
MechaCat02
2026-07-02 20:23:56 +02:00
parent 43441523f9
commit 3559c8f6ef
9 changed files with 315 additions and 111 deletions

View File

@@ -238,6 +238,13 @@ pub struct XenosPipeline {
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,
@@ -315,26 +322,29 @@ impl XenosPipeline {
},
],
});
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: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
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,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
entries: &tex_bgl_entries,
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
@@ -437,19 +447,21 @@ impl XenosPipeline {
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: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&dummy_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&dummy_sampler),
},
],
entries: &tex_bg_entries,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -731,20 +743,35 @@ impl XenosPipeline {
/// [`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>) {
let bound = view.unwrap_or(&self.dummy_view);
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 (rebind)"),
label: Some("xenos tex bind group (slots)"),
layout: &self.tex_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(bound),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
entries: &entries,
});
}