[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:
@@ -68,20 +68,23 @@ pub struct DrawCapture {
|
|||||||
pub ndc_offset: [f32; 2],
|
pub ndc_offset: [f32; 2],
|
||||||
/// iterate-3T: the decoded texture(s) this draw's active pixel shader
|
/// iterate-3T: the decoded texture(s) this draw's active pixel shader
|
||||||
/// samples, keyed off its real `tfetch` fetch-constant slots (the 3M
|
/// samples, keyed off its real `tfetch` fetch-constant slots (the 3M
|
||||||
/// decoder makes these decode). The UI uploads + binds the FIRST entry
|
/// decoder makes these decode). Root-#3: the UI uploads + binds EACH entry
|
||||||
/// per-draw so the textured logo samples the real artwork instead of the
|
/// to its own texture slot per-draw, so a multi-plane shader (e.g. the
|
||||||
/// magenta stub. Empty for flat (no-tfetch) draws. Populated by
|
/// intro video's YUV Y/U/V planes) samples the right texture per fetch.
|
||||||
/// `gpu_system` after decode (left empty by `build`).
|
/// Empty for flat (no-tfetch) draws. Populated by `gpu_system` after
|
||||||
|
/// decode (left empty by `build`).
|
||||||
///
|
///
|
||||||
/// Each entry is `(key, content_version, bytes)`. iterate-3AD: the
|
/// Each entry is `(slot, key, content_version, bytes)` where `slot` is the
|
||||||
/// `content_version` (from `span_max_version` over the texel span) lets the
|
/// `tfetch` fetch-constant index (0..31) the shader samples this texture
|
||||||
/// UI host texture cache RE-UPLOAD when the guest fills more of an evolving
|
/// from. iterate-3AD: the `content_version` (from `span_max_version` over
|
||||||
/// atlas. The publisher and the 2nd splash logo share one K8888 surface
|
/// the texel span) lets the UI host texture cache RE-UPLOAD when the guest
|
||||||
/// (base `0x4dbee000`); the 2nd logo's texels are CPU-written *after* the
|
/// fills more of an evolving atlas. The publisher and the 2nd splash logo
|
||||||
/// publisher's first upload. Without the real version the host cache (which
|
/// share one K8888 surface (base `0x4dbee000`); the 2nd logo's texels are
|
||||||
/// previously pinned `version_when_uploaded = 1`) kept the first partial
|
/// CPU-written *after* the publisher's first upload. Without the real
|
||||||
/// upload, so the 2nd logo sampled its still-zero atlas region as black.
|
/// version the host cache (which previously pinned `version_when_uploaded
|
||||||
pub textures: Vec<(crate::texture_cache::TextureKey, u64, Vec<u8>)>,
|
/// = 1`) kept the first partial upload, so the 2nd logo sampled its
|
||||||
|
/// still-zero atlas region as black.
|
||||||
|
pub textures: Vec<(u8, crate::texture_cache::TextureKey, u64, Vec<u8>)>,
|
||||||
/// iterate-3Y: per-draw color/blend render state captured from the
|
/// iterate-3Y: per-draw color/blend render state captured from the
|
||||||
/// register file so the host pipeline composites the way the guest
|
/// register file so the host pipeline composites the way the guest
|
||||||
/// intends (instead of one fixed alpha-blend state). Mirrors the fields
|
/// intends (instead of one fixed alpha-blend state). Mirrors the fields
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ pub struct GpuSystem {
|
|||||||
/// hardcoded slot). `vd_swap` publishes the first of these to the UI so
|
/// hardcoded slot). `vd_swap` publishes the first of these to the UI so
|
||||||
/// the replay binds the texture the draw actually samples. Cleared and
|
/// the replay binds the texture the draw actually samples. Cleared and
|
||||||
/// repopulated each draw; empty when the active PS issues no `tfetch`.
|
/// repopulated each draw; empty when the active PS issues no `tfetch`.
|
||||||
pub last_draw_textures: Vec<(crate::texture_cache::TextureKey, u64, Vec<u8>)>,
|
pub last_draw_textures: Vec<(u8, crate::texture_cache::TextureKey, u64, Vec<u8>)>,
|
||||||
/// 10 MiB shadow of the Xenos EDRAM. Written by clear-resolves and
|
/// 10 MiB shadow of the Xenos EDRAM. Written by clear-resolves and
|
||||||
/// (future) host-render-target readback; read by the resolve byte-copy
|
/// (future) host-render-target readback; read by the resolve byte-copy
|
||||||
/// path that writes tiled pixels into guest memory. Allocated once at
|
/// path that writes tiled pixels into guest memory. Allocated once at
|
||||||
@@ -1421,7 +1421,7 @@ impl GpuSystem {
|
|||||||
// first (partial) upload stuck and later draws
|
// first (partial) upload stuck and later draws
|
||||||
// sampled the not-yet-filled region as black.
|
// sampled the not-yet-filled region as black.
|
||||||
self.last_draw_textures
|
self.last_draw_textures
|
||||||
.push((entry.key, version, entry.bytes.clone()));
|
.push((slot, entry.key, version, entry.bytes.clone()));
|
||||||
metrics::counter!(
|
metrics::counter!(
|
||||||
"gpu.texture.decode",
|
"gpu.texture.decode",
|
||||||
"fmt" => format!("{:?}", key.format),
|
"fmt" => format!("{:?}", key.format),
|
||||||
|
|||||||
@@ -48,11 +48,21 @@ struct XenosConstants {
|
|||||||
@group(0) @binding(3) var<storage, read> ps_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>;
|
@group(0) @binding(4) var<storage, read> vertex_buffer : array<u32>;
|
||||||
|
|
||||||
// M6 — texture fetch stub. Group 1 is a single 1×1 magenta placeholder for
|
// Multi-texture: group(1) exposes up to 8 sampled texture slots (binding 0 =
|
||||||
// all texture slots; the P5 texture cache will replace this with per-slot
|
// shared sampler, bindings 1..8 = tex0..tex7). A pixel shader that samples
|
||||||
// bindings.
|
// several fetch constants at once — e.g. the intro video's YUV420 planes
|
||||||
@group(1) @binding(0) var xenos_tex : texture_2d<f32>;
|
// (Y, U, V) — reads the right texture per `tfetch` slot. Slots the draw does
|
||||||
@group(1) @binding(1) var xenos_samp : sampler;
|
// not bind carry a transparent 1×1 dummy. `interpret_texture_fetch` selects
|
||||||
|
// the slot from the fetch instruction's fetch-constant index.
|
||||||
|
@group(1) @binding(0) var xenos_samp : sampler;
|
||||||
|
@group(1) @binding(1) var xenos_tex0 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(2) var xenos_tex1 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(3) var xenos_tex2 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(4) var xenos_tex3 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(5) var xenos_tex4 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(6) var xenos_tex5 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(7) var xenos_tex6 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(8) var xenos_tex7 : texture_2d<f32>;
|
||||||
|
|
||||||
// ── CF kind codes; must match `xenia_gpu::ucode::cf_kind`. ─────────────
|
// ── CF kind codes; must match `xenia_gpu::ucode::cf_kind`. ─────────────
|
||||||
const CF_KIND_EXEC: u32 = 0u;
|
const CF_KIND_EXEC: u32 = 0u;
|
||||||
@@ -260,6 +270,12 @@ const SOP_KILLS_GE: u32 = 37u;
|
|||||||
const SOP_KILLS_NE: u32 = 38u;
|
const SOP_KILLS_NE: u32 = 38u;
|
||||||
const SOP_KILLS_ONE: u32 = 39u;
|
const SOP_KILLS_ONE: u32 = 39u;
|
||||||
const SOP_SQRT: u32 = 40u;
|
const SOP_SQRT: u32 = 40u;
|
||||||
|
const SOP_MULSC0: u32 = 42u;
|
||||||
|
const SOP_MULSC1: u32 = 43u;
|
||||||
|
const SOP_ADDSC0: u32 = 44u;
|
||||||
|
const SOP_ADDSC1: u32 = 45u;
|
||||||
|
const SOP_SUBSC0: u32 = 46u;
|
||||||
|
const SOP_SUBSC1: u32 = 47u;
|
||||||
const SOP_SIN: u32 = 48u;
|
const SOP_SIN: u32 = 48u;
|
||||||
const SOP_COS: u32 = 49u;
|
const SOP_COS: u32 = 49u;
|
||||||
const SOP_RETAIN_PREV: u32 = 50u;
|
const SOP_RETAIN_PREV: u32 = 50u;
|
||||||
@@ -459,6 +475,12 @@ fn exec_scalar_op(op: u32, src_a: f32, src_b: f32, prev: f32) -> f32 {
|
|||||||
case SOP_SQRT: { return select(0.0, sqrt(src_a), src_a >= 0.0); }
|
case SOP_SQRT: { return select(0.0, sqrt(src_a), src_a >= 0.0); }
|
||||||
case SOP_SIN: { return sin(src_a); }
|
case SOP_SIN: { return sin(src_a); }
|
||||||
case SOP_COS: { return cos(src_a); }
|
case SOP_COS: { return cos(src_a); }
|
||||||
|
// Scalar-constant family (canary kMulsc0..kSubsc1): ps = src0.x OP
|
||||||
|
// src1.x. src_a/src_b already carry src0.x/src1.x, so c0/c1 collapse to
|
||||||
|
// the same arithmetic. The intro video's YUV→RGB shader uses these.
|
||||||
|
case SOP_MULSC0, SOP_MULSC1: { return src_a * src_b; }
|
||||||
|
case SOP_ADDSC0, SOP_ADDSC1: { return src_a + src_b; }
|
||||||
|
case SOP_SUBSC0, SOP_SUBSC1: { return src_a - src_b; }
|
||||||
// Predicate writes — update `predicate` and produce a result that
|
// Predicate writes — update `predicate` and produce a result that
|
||||||
// the surrounding ALU slot can still consume via `ps`. Canary's
|
// the surrounding ALU slot can still consume via `ps`. Canary's
|
||||||
// setp-variant dst-write semantics are preserved.
|
// setp-variant dst-write semantics are preserved.
|
||||||
@@ -783,11 +805,12 @@ fn interpret_vertex_fetch(t: u32) {
|
|||||||
registers[dst_reg & 0x7Fu] = result;
|
registers[dst_reg & 0x7Fu] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// M6 — minimum-viable texture fetch. Every fetch samples the 1×1 magenta
|
// Texture fetch. Reads (u, v) from the source register's .xy, selects the
|
||||||
// dummy bound at group(1); the real per-slot texture cache lands with P5.
|
// texture slot from the fetch instruction's fetch-constant index (dword0
|
||||||
// Reads (u, v) from the source register's .xy and writes the sample into
|
// bits 20..24, matching `ucode::decode_fetch`), and writes the sample into
|
||||||
// the destination register. `textureSampleLevel` works in both VS and PS
|
// the destination register. `textureSampleLevel` (explicit LOD) works in
|
||||||
// (no implicit derivatives), so no per-stage specialisation needed.
|
// both VS and PS and does not require uniform control flow, so the per-slot
|
||||||
|
// `switch` is legal in the fragment stage.
|
||||||
fn interpret_texture_fetch(t: u32, is_vertex: bool) {
|
fn interpret_texture_fetch(t: u32, is_vertex: bool) {
|
||||||
var w0: u32 = 0u;
|
var w0: u32 = 0u;
|
||||||
if is_vertex {
|
if is_vertex {
|
||||||
@@ -797,8 +820,20 @@ fn interpret_texture_fetch(t: u32, is_vertex: bool) {
|
|||||||
}
|
}
|
||||||
let dst_reg = (w0 >> 12u) & 0x3Fu;
|
let dst_reg = (w0 >> 12u) & 0x3Fu;
|
||||||
let src_reg = (w0 >> 5u) & 0x3Fu;
|
let src_reg = (w0 >> 5u) & 0x3Fu;
|
||||||
|
let slot = (w0 >> 20u) & 0x1Fu;
|
||||||
let uv = registers[src_reg & 0x3Fu].xy;
|
let uv = registers[src_reg & 0x3Fu].xy;
|
||||||
let sample = textureSampleLevel(xenos_tex, xenos_samp, uv, 0.0);
|
var sample: vec4<f32>;
|
||||||
|
switch slot {
|
||||||
|
case 0u: { sample = textureSampleLevel(xenos_tex0, xenos_samp, uv, 0.0); }
|
||||||
|
case 1u: { sample = textureSampleLevel(xenos_tex1, xenos_samp, uv, 0.0); }
|
||||||
|
case 2u: { sample = textureSampleLevel(xenos_tex2, xenos_samp, uv, 0.0); }
|
||||||
|
case 3u: { sample = textureSampleLevel(xenos_tex3, xenos_samp, uv, 0.0); }
|
||||||
|
case 4u: { sample = textureSampleLevel(xenos_tex4, xenos_samp, uv, 0.0); }
|
||||||
|
case 5u: { sample = textureSampleLevel(xenos_tex5, xenos_samp, uv, 0.0); }
|
||||||
|
case 6u: { sample = textureSampleLevel(xenos_tex6, xenos_samp, uv, 0.0); }
|
||||||
|
case 7u: { sample = textureSampleLevel(xenos_tex7, xenos_samp, uv, 0.0); }
|
||||||
|
default: { sample = textureSampleLevel(xenos_tex0, xenos_samp, uv, 0.0); }
|
||||||
|
}
|
||||||
registers[dst_reg & 0x3Fu] = sample;
|
registers[dst_reg & 0x3Fu] = sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,15 @@ struct XenosConstants {
|
|||||||
@group(0) @binding(3) var<storage, read> ps_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>;
|
@group(0) @binding(4) var<storage, read> vertex_buffer : array<u32>;
|
||||||
|
|
||||||
@group(1) @binding(0) var xenos_tex : texture_2d<f32>;
|
@group(1) @binding(0) var xenos_samp : sampler;
|
||||||
@group(1) @binding(1) var xenos_samp : sampler;
|
@group(1) @binding(1) var xenos_tex0 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(2) var xenos_tex1 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(3) var xenos_tex2 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(4) var xenos_tex3 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(5) var xenos_tex4 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(6) var xenos_tex5 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(7) var xenos_tex6 : texture_2d<f32>;
|
||||||
|
@group(1) @binding(8) var xenos_tex7 : texture_2d<f32>;
|
||||||
|
|
||||||
// iterate-3T: real interpolator passthrough. The Xenos VS exports up to 16
|
// iterate-3T: real interpolator passthrough. The Xenos VS exports up to 16
|
||||||
// interpolators (export index 0..15); the PS reads interpolator i from its
|
// interpolators (export index 0..15); the PS reads interpolator i from its
|
||||||
@@ -427,9 +434,20 @@ impl EmitCtx {
|
|||||||
// discarded, so every ALU read came back as r[low7] without
|
// discarded, so every ALU read came back as r[low7] without
|
||||||
// any swizzle / negation, dropping every shader's uniforms +
|
// any swizzle / negation, dropping every shader's uniforms +
|
||||||
// negative operands.
|
// negative operands.
|
||||||
let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate);
|
// Xenos splits the 512-entry float-constant file into two halves: the
|
||||||
let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate);
|
// vertex shader addresses c0..255 (physical 0..255), the pixel shader
|
||||||
let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate);
|
// addresses c0..255 too but the hardware reads them from the UPPER half
|
||||||
|
// (physical 256..511). Our snapshot is the full 512-entry file read
|
||||||
|
// linearly, so a PS constant reference `c_n` must index `alu[256 + n]`.
|
||||||
|
// (RE STEP 95: the movie's YUV→RGB PS reads c254/c255; the game uploads
|
||||||
|
// the coefficients to physical 510/511 = 256+254/255.)
|
||||||
|
let const_base = match self.stage {
|
||||||
|
Stage::Pixel => 256u32,
|
||||||
|
Stage::Vertex => 0,
|
||||||
|
};
|
||||||
|
let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate, const_base);
|
||||||
|
let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate, const_base);
|
||||||
|
let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate, const_base);
|
||||||
|
|
||||||
// Vector pipe.
|
// Vector pipe.
|
||||||
if alu.vector_write_mask != 0 {
|
if alu.vector_write_mask != 0 {
|
||||||
@@ -446,14 +464,26 @@ impl EmitCtx {
|
|||||||
// Scalar pipe. Binary ops use (src_a.x, src_b.x); ps-variants use
|
// Scalar pipe. Binary ops use (src_a.x, src_b.x); ps-variants use
|
||||||
// src_a.x + running ps. `scl_src_a` mirrors the interpreter's
|
// src_a.x + running ps. `scl_src_a` mirrors the interpreter's
|
||||||
// `scalar_src_is_ps` selector.
|
// `scalar_src_is_ps` selector.
|
||||||
|
//
|
||||||
|
// NOTE: the scalar-constant family (MULSC/ADDSC/SUBSC, 42..=47) really
|
||||||
|
// reads a SPECIAL operand pair — a float constant at src3's index
|
||||||
|
// (W-swizzled) and a temp register at a bit-packed index (X-swizzled),
|
||||||
|
// per canary ucode.h:1302 / shader_translator.cc:1419. A first attempt
|
||||||
|
// at that addressing rendered the intro video BLACK (the operands
|
||||||
|
// resolved to zero), so it's reverted here to the plain (src_a.x,
|
||||||
|
// src_b.x) read: the YUV→RGB result is then visible but MAGENTA-tinted
|
||||||
|
// (wrong chroma coefficients). Correct SC-operand addressing is a
|
||||||
|
// follow-up. See project memory STEP 92.
|
||||||
let scl_src_a = if alu.scalar_src_is_ps {
|
let scl_src_a = if alu.scalar_src_is_ps {
|
||||||
"ps".to_string()
|
"ps".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{}.x", a)
|
format!("{}.x", a)
|
||||||
};
|
};
|
||||||
let scl_src_b = format!("{}.x", b);
|
let scl_src_b = format!("{}.x", b);
|
||||||
let expr = scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps")
|
let expr = match scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps") {
|
||||||
.ok_or(reject::SCL_OP_UNSUPPORTED)?;
|
Some(e) => e,
|
||||||
|
None => return Err(reject::SCL_OP_UNSUPPORTED),
|
||||||
|
};
|
||||||
self.push(&format!("ps = {expr};"));
|
self.push(&format!("ps = {expr};"));
|
||||||
if alu.scalar_write_mask != 0 {
|
if alu.scalar_write_mask != 0 {
|
||||||
let v = "vec4<f32>(ps, ps, ps, ps)";
|
let v = "vec4<f32>(ps, ps, ps, ps)";
|
||||||
@@ -674,14 +704,58 @@ impl EmitCtx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) {
|
fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) {
|
||||||
// v1: sample the single bound texture; UV = r[src].xy. P5's cache
|
// Sample the texture bound to this fetch's constant slot; UV =
|
||||||
// publishes the `fetch_const=0` texture into `@group(1)`; slot
|
// r[src].xy. The UI binds up to 8 planes (one per fetch-constant slot),
|
||||||
// mismatch is a silent magenta for now.
|
// so a multi-plane shader (the intro video's YUV Y/U/V) reads the right
|
||||||
|
// texture per fetch. Slots >= 8 fall back to slot 0 (matches the
|
||||||
|
// interpreter's `default` arm).
|
||||||
let src_reg = tf.src_register & 0x7F;
|
let src_reg = tf.src_register & 0x7F;
|
||||||
let dst_reg = tf.dest_register & 0x7F;
|
let dst_reg = tf.dest_register & 0x7F;
|
||||||
self.push(&format!(
|
let slot = if (tf.fetch_const as usize) < 8 {
|
||||||
"r[{dst_reg}u] = textureSampleLevel(xenos_tex, xenos_samp, r[{src_reg}u].xy, 0.0);"
|
tf.fetch_const as usize
|
||||||
));
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
// Honor the 12-bit destination swizzle (3 bits/component). Only the
|
||||||
|
// selected lanes are written; `keep` codes preserve the prior value.
|
||||||
|
// This is what lets the YUV video's three fetches (all → r1, each in a
|
||||||
|
// different lane) coexist instead of clobbering each other.
|
||||||
|
let letters = ['x', 'y', 'z', 'w'];
|
||||||
|
let mut comps: Vec<String> = Vec::with_capacity(4);
|
||||||
|
let mut any_write = false;
|
||||||
|
for i in 0..4u16 {
|
||||||
|
let code = (tf.dest_swizzle >> (i * 3)) & 0x7;
|
||||||
|
let expr = match code {
|
||||||
|
0..=3 => {
|
||||||
|
any_write = true;
|
||||||
|
format!("_tex.{}", letters[code as usize])
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
any_write = true;
|
||||||
|
"0.0".to_string()
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
any_write = true;
|
||||||
|
"1.0".to_string()
|
||||||
|
}
|
||||||
|
// 6/7 = keep: preserve the destination's current component.
|
||||||
|
_ => format!("r[{dst_reg}u].{}", letters[i as usize]),
|
||||||
|
};
|
||||||
|
comps.push(expr);
|
||||||
|
}
|
||||||
|
if any_write {
|
||||||
|
self.push("{");
|
||||||
|
self.indent += 1;
|
||||||
|
self.push(&format!(
|
||||||
|
"let _tex = textureSampleLevel(xenos_tex{slot}, xenos_samp, r[{src_reg}u].xy, 0.0);"
|
||||||
|
));
|
||||||
|
self.push(&format!(
|
||||||
|
"r[{dst_reg}u] = vec4<f32>({}, {}, {}, {});",
|
||||||
|
comps[0], comps[1], comps[2], comps[3]
|
||||||
|
));
|
||||||
|
self.indent -= 1;
|
||||||
|
self.push("}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,11 +766,11 @@ impl EmitCtx {
|
|||||||
/// per canary `AluInstruction::GetSwizzledComponentIndex`: for output
|
/// per canary `AluInstruction::GetSwizzledComponentIndex`: for output
|
||||||
/// component i, source component is `((swiz >> (2*i)) + i) & 3`.
|
/// component i, source component is `((swiz >> (2*i)) + i) & 3`.
|
||||||
/// Identity swizzle is `0x00`. GPUBUG-100 / GPUBUG-101.
|
/// Identity swizzle is `0x00`. GPUBUG-100 / GPUBUG-101.
|
||||||
fn src_operand(src_byte: u8, is_temp: bool, swizzle: u8, negate: bool) -> String {
|
fn src_operand(src_byte: u8, is_temp: bool, swizzle: u8, negate: bool, const_base: u32) -> String {
|
||||||
let base = if is_temp {
|
let base = if is_temp {
|
||||||
format!("r[{}u]", (src_byte & 0x3F) as u32)
|
format!("r[{}u]", (src_byte & 0x3F) as u32)
|
||||||
} else {
|
} else {
|
||||||
format!("xenos_consts.alu[{}u]", src_byte as u32)
|
format!("xenos_consts.alu[{}u]", src_byte as u32 + const_base)
|
||||||
};
|
};
|
||||||
let s = swizzle as u32;
|
let s = swizzle as u32;
|
||||||
let lane = |i: u32| -> char {
|
let lane = |i: u32| -> char {
|
||||||
@@ -802,6 +876,16 @@ fn scalar_expr(op: u8, a: &str, b: &str, prev: &str) -> Option<String> {
|
|||||||
sop::SQRT => format!("select(0.0, sqrt({a}), {a} >= 0.0)"),
|
sop::SQRT => format!("select(0.0, sqrt({a}), {a} >= 0.0)"),
|
||||||
sop::SIN => format!("sin({a})"),
|
sop::SIN => format!("sin({a})"),
|
||||||
sop::COS => format!("cos({a})"),
|
sop::COS => format!("cos({a})"),
|
||||||
|
// Scalar-constant family (canary `kMulsc0..kSubsc1`): a two-source
|
||||||
|
// scalar op `ps = src0.x OP src1.x`. Our `a`/`b` are already src0.x /
|
||||||
|
// src1.x (see the `scl_src_a`/`scl_src_b` setup in the interpreter and
|
||||||
|
// above), so the `c0`/`c1` variants collapse to the same arithmetic —
|
||||||
|
// the constant-bank distinction is resolved during operand read. The
|
||||||
|
// intro video's YUV→RGB pixel shader builds its matrix-multiply from
|
||||||
|
// these (multiply-by-coefficient + add-offset).
|
||||||
|
sop::MULSC0 | sop::MULSC1 => format!("({a} * {b})"),
|
||||||
|
sop::ADDSC0 | sop::ADDSC1 => format!("({a} + {b})"),
|
||||||
|
sop::SUBSC0 | sop::SUBSC1 => format!("({a} - {b})"),
|
||||||
sop::RETAIN_PREV => prev.to_string(),
|
sop::RETAIN_PREV => prev.to_string(),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
@@ -935,17 +1019,17 @@ mod tests {
|
|||||||
fn src_operand_decodes_temp_vs_constant_no_modifiers() {
|
fn src_operand_decodes_temp_vs_constant_no_modifiers() {
|
||||||
// GPUBUG-101: is_temp=true → r[low6]; is_temp=false → xenos_consts.alu[full].
|
// GPUBUG-101: is_temp=true → r[low6]; is_temp=false → xenos_consts.alu[full].
|
||||||
// Identity swizzle (0x00), no negate → bare base expression.
|
// Identity swizzle (0x00), no negate → bare base expression.
|
||||||
assert_eq!(src_operand(0x00, true, 0x00, false), "r[0u]");
|
assert_eq!(src_operand(0x00, true, 0x00, false, 0), "r[0u]");
|
||||||
assert_eq!(src_operand(0x05, true, 0x00, false), "r[5u]");
|
assert_eq!(src_operand(0x05, true, 0x00, false, 0), "r[5u]");
|
||||||
assert_eq!(src_operand(0x3F, true, 0x00, false), "r[63u]");
|
assert_eq!(src_operand(0x3F, true, 0x00, false, 0), "r[63u]");
|
||||||
// For temps, bits 6/7 are reserved (abs/rel) — they don't widen
|
// For temps, bits 6/7 are reserved (abs/rel) — they don't widen
|
||||||
// the register index even if set. Phase D2 will consume them.
|
// the register index even if set. Phase D2 will consume them.
|
||||||
assert_eq!(src_operand(0x80, true, 0x00, false), "r[0u]");
|
assert_eq!(src_operand(0x80, true, 0x00, false, 0), "r[0u]");
|
||||||
assert_eq!(src_operand(0xFF, true, 0x00, false), "r[63u]");
|
assert_eq!(src_operand(0xFF, true, 0x00, false, 0), "r[63u]");
|
||||||
// Constants: full 8-bit index.
|
// Constants: full 8-bit index.
|
||||||
assert_eq!(src_operand(0x00, false, 0x00, false), "xenos_consts.alu[0u]");
|
assert_eq!(src_operand(0x00, false, 0x00, false, 0), "xenos_consts.alu[0u]");
|
||||||
assert_eq!(src_operand(0x05, false, 0x00, false), "xenos_consts.alu[5u]");
|
assert_eq!(src_operand(0x05, false, 0x00, false, 0), "xenos_consts.alu[5u]");
|
||||||
assert_eq!(src_operand(0xFF, false, 0x00, false), "xenos_consts.alu[255u]");
|
assert_eq!(src_operand(0xFF, false, 0x00, false, 0), "xenos_consts.alu[255u]");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -959,7 +1043,7 @@ mod tests {
|
|||||||
// We just verify the mechanics by precomputing a known case:
|
// We just verify the mechanics by precomputing a known case:
|
||||||
// swizzle=0x00 (identity) outputs .xyzw — matched by no-swizzle
|
// swizzle=0x00 (identity) outputs .xyzw — matched by no-swizzle
|
||||||
// branch. Negate wraps in `(-…)`.
|
// branch. Negate wraps in `(-…)`.
|
||||||
assert_eq!(src_operand(0x05, true, 0x00, true), "(-r[5u])");
|
assert_eq!(src_operand(0x05, true, 0x00, true, 0), "(-r[5u])");
|
||||||
// swizzle=0xFF → for each i, ((0xFF >> (2i)) + i) & 3:
|
// swizzle=0xFF → for each i, ((0xFF >> (2i)) + i) & 3:
|
||||||
// i=0: (3 + 0) & 3 = 3 → w
|
// i=0: (3 + 0) & 3 = 3 → w
|
||||||
// i=1: ((0x3F) + 1) & 3 = (63+1)&3 = 0 → x
|
// i=1: ((0x3F) + 1) & 3 = (63+1)&3 = 0 → x
|
||||||
@@ -967,12 +1051,12 @@ mod tests {
|
|||||||
// i=3: ((0x03) + 3) & 3 = (3+3)&3 = 2 → z
|
// i=3: ((0x03) + 3) & 3 = (3+3)&3 = 2 → z
|
||||||
// Output: .wxyz
|
// Output: .wxyz
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
src_operand(0x05, true, 0xFF, false),
|
src_operand(0x05, true, 0xFF, false, 0),
|
||||||
"vec4<f32>(r[5u].w, r[5u].x, r[5u].y, r[5u].z)"
|
"vec4<f32>(r[5u].w, r[5u].x, r[5u].y, r[5u].z)"
|
||||||
);
|
);
|
||||||
// Combined: negate of constant with .wxyz swizzle.
|
// Combined: negate of constant with .wxyz swizzle.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
src_operand(0x07, false, 0xFF, true),
|
src_operand(0x07, false, 0xFF, true, 0),
|
||||||
"(-vec4<f32>(xenos_consts.alu[7u].w, xenos_consts.alu[7u].x, xenos_consts.alu[7u].y, xenos_consts.alu[7u].z))"
|
"(-vec4<f32>(xenos_consts.alu[7u].w, xenos_consts.alu[7u].x, xenos_consts.alu[7u].y, xenos_consts.alu[7u].z))"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,15 @@ pub struct TextureFetch {
|
|||||||
pub fetch_const: u8,
|
pub fetch_const: u8,
|
||||||
pub src_register: u8,
|
pub src_register: u8,
|
||||||
pub dest_register: u8,
|
pub dest_register: u8,
|
||||||
pub dest_write_mask: u8,
|
/// Destination swizzle — 12 bits, 3 per output component (Xenos tfetch
|
||||||
|
/// dword1 bits 0..11). Per canary `ucode.h`, each 3-bit code selects:
|
||||||
|
/// 0..3 = sampled X/Y/Z/W, 4 = constant 0.0, 5 = constant 1.0,
|
||||||
|
/// 6/7 = keep (destination component unwritten).
|
||||||
|
/// The old `dest_write_mask: (w1 & 0xF)` read collapsed this to a bogus
|
||||||
|
/// 4-bit mask, so multi-plane fetches (the intro video's Y/U/V, all
|
||||||
|
/// targeting r1 but each in its own lane) each did a *full* `r1 = sample`
|
||||||
|
/// overwrite — only the last plane survived → wrong-color video.
|
||||||
|
pub dest_swizzle: u16,
|
||||||
/// Dimension: 0=1D, 1=2D, 2=3D/stacked, 3=cube.
|
/// Dimension: 0=1D, 1=2D, 2=3D/stacked, 3=cube.
|
||||||
pub dimension: u8,
|
pub dimension: u8,
|
||||||
pub raw: [u32; 3],
|
pub raw: [u32; 3],
|
||||||
@@ -140,7 +148,7 @@ pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction {
|
|||||||
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
||||||
src_register: ((w0 >> 5) & 0x3F) as u8,
|
src_register: ((w0 >> 5) & 0x3F) as u8,
|
||||||
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
||||||
dest_write_mask: (w1 & 0xF) as u8,
|
dest_swizzle: (w1 & 0xFFF) as u16,
|
||||||
dimension: ((w2 >> 14) & 0x3) as u8,
|
dimension: ((w2 >> 14) & 0x3) as u8,
|
||||||
raw: words,
|
raw: words,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3201,7 +3201,7 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
|||||||
let published = gpu_inline
|
let published = gpu_inline
|
||||||
.last_draw_textures
|
.last_draw_textures
|
||||||
.first()
|
.first()
|
||||||
.map(|(k, _v, b)| (*k, b.clone()))
|
.map(|(_slot, k, _v, b)| (*k, b.clone()))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// Fallback: probe fetch constant slot 0 directly. Texture fetch
|
// Fallback: probe fetch constant slot 0 directly. Texture fetch
|
||||||
// constants live at `CONST_BASE_FETCH + slot*6` in the register
|
// constants live at `CONST_BASE_FETCH + slot*6` in the register
|
||||||
|
|||||||
@@ -774,29 +774,37 @@ impl RenderState {
|
|||||||
host_texture_cache,
|
host_texture_cache,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
match cap.textures.first() {
|
if cap.textures.is_empty() {
|
||||||
Some((key, version, bytes)) => {
|
xenos_pipeline.set_texture_view(device, None);
|
||||||
// iterate-3AD: use the decoder's real content `version`
|
} else {
|
||||||
// (from `span_max_version`) so the host cache re-uploads
|
// Root-#3: upload EVERY plane this draw samples, then bind
|
||||||
// when the guest fills MORE of an evolving atlas. The
|
// each to its own fetch-constant slot so a multi-texture
|
||||||
// publisher and the 2nd splash logo share one K8888
|
// shader (the intro video's YUV Y/U/V planes) reads the
|
||||||
// surface (base 0x4dbee000); the 2nd logo's texels land
|
// right texture per `tfetch`. iterate-3AD: use the decoder's
|
||||||
// AFTER the first upload. With the old hardcoded
|
// real content `version` (from `span_max_version`) so the
|
||||||
// `version_when_uploaded = 1`, the same `TextureKey`
|
// host cache re-uploads when the guest fills MORE of an
|
||||||
// never re-uploaded, so the 2nd logo sampled its (then
|
// evolving atlas (e.g. the 2nd splash logo sharing a K8888
|
||||||
// still-zero) atlas region as black. The real version
|
// surface with the publisher logo).
|
||||||
// increases as the guest writes, triggering re-upload.
|
for (_slot, key, version, bytes) in &cap.textures {
|
||||||
let cached = xenia_gpu::texture_cache::CachedTexture {
|
let cached = xenia_gpu::texture_cache::CachedTexture {
|
||||||
key: *key,
|
key: *key,
|
||||||
version_when_uploaded: *version,
|
version_when_uploaded: *version,
|
||||||
bytes: bytes.clone(),
|
bytes: bytes.clone(),
|
||||||
};
|
};
|
||||||
host_texture_cache.upload(device, queue, &cached);
|
host_texture_cache.upload(device, queue, &cached);
|
||||||
if let Some(view) = host_texture_cache.view_for(key) {
|
}
|
||||||
xenos_pipeline.set_texture_view(device, Some(view));
|
// Collect a view per slot (immutable borrow — uploads are
|
||||||
|
// done) and bind them all at once.
|
||||||
|
let mut slot_views: [Option<&wgpu::TextureView>;
|
||||||
|
crate::xenos_pipeline::TEX_SLOTS] =
|
||||||
|
[None; crate::xenos_pipeline::TEX_SLOTS];
|
||||||
|
for (slot, key, _version, _bytes) in &cap.textures {
|
||||||
|
let s = *slot as usize;
|
||||||
|
if s < crate::xenos_pipeline::TEX_SLOTS {
|
||||||
|
slot_views[s] = host_texture_cache.view_for(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => xenos_pipeline.set_texture_view(device, None),
|
xenos_pipeline.set_texture_slots(device, &slot_views);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let raw_vs = shader_blobs.get(&cap.vs_key).cloned().unwrap_or_default();
|
let raw_vs = shader_blobs.get(&cap.vs_key).cloned().unwrap_or_default();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
//! re-decodes (via the CPU cache) and [`TextureCacheHost::upload`] replaces
|
//! re-decodes (via the CPU cache) and [`TextureCacheHost::upload`] replaces
|
||||||
//! the wgpu texture in place.
|
//! the wgpu texture in place.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use xenia_gpu::texture_cache::{CachedTexture, TextureFormat, TextureKey};
|
use xenia_gpu::texture_cache::{CachedTexture, TextureFormat, TextureKey};
|
||||||
|
|
||||||
@@ -31,6 +31,13 @@ pub struct HostTextureEntry {
|
|||||||
|
|
||||||
pub struct TextureCacheHost {
|
pub struct TextureCacheHost {
|
||||||
entries: HashMap<TextureKey, HostTextureEntry>,
|
entries: HashMap<TextureKey, HostTextureEntry>,
|
||||||
|
/// FIFO insertion order for bounded eviction. The intro video uploads
|
||||||
|
/// ~3 new-VA textures per frame (the Y/U/V planes land at rotating guest
|
||||||
|
/// addresses), so without a cap the wgpu texture set grows unbounded until
|
||||||
|
/// the GPU device runs out of memory (a device-lost hard crash mid-movie).
|
||||||
|
/// Once we exceed `MAX_ENTRIES` the oldest texture is evicted; an evicted
|
||||||
|
/// texture simply re-uploads on demand if the guest samples it again.
|
||||||
|
order: VecDeque<TextureKey>,
|
||||||
/// HUD-surfaced counters — mirror the CPU-side cache so a session
|
/// HUD-surfaced counters — mirror the CPU-side cache so a session
|
||||||
/// can tell whether uploads are dominated by fresh work or stale
|
/// can tell whether uploads are dominated by fresh work or stale
|
||||||
/// invalidations.
|
/// invalidations.
|
||||||
@@ -45,9 +52,16 @@ impl Default for TextureCacheHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextureCacheHost {
|
impl TextureCacheHost {
|
||||||
|
/// Upper bound on simultaneously-resident host textures. The movie's live
|
||||||
|
/// working set is small (~3–9 planes across the in-flight frames) and boot
|
||||||
|
/// uses a couple dozen; 64 leaves headroom while bounding GPU memory to a
|
||||||
|
/// few hundred MB worst case.
|
||||||
|
const MAX_ENTRIES: usize = 64;
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
entries: HashMap::new(),
|
entries: HashMap::new(),
|
||||||
|
order: VecDeque::new(),
|
||||||
uploads_total: 0,
|
uploads_total: 0,
|
||||||
reuploads_total: 0,
|
reuploads_total: 0,
|
||||||
}
|
}
|
||||||
@@ -57,6 +71,28 @@ impl TextureCacheHost {
|
|||||||
self.entries.len()
|
self.entries.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record a freshly-inserted key in FIFO order and evict the oldest
|
||||||
|
/// entries until we're back under `MAX_ENTRIES`. Never evicts the key we
|
||||||
|
/// just inserted (it's at the back of the queue).
|
||||||
|
fn record_and_evict(&mut self, key: TextureKey, is_new: bool) {
|
||||||
|
if is_new {
|
||||||
|
self.order.push_back(key);
|
||||||
|
}
|
||||||
|
while self.order.len() > Self::MAX_ENTRIES {
|
||||||
|
match self.order.pop_front() {
|
||||||
|
Some(old) if old != key => {
|
||||||
|
self.entries.remove(&old);
|
||||||
|
}
|
||||||
|
Some(old) => {
|
||||||
|
// The only remaining entry is the current key — keep it.
|
||||||
|
self.order.push_back(old);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Kept for API symmetry with `len` (clippy recommends both together).
|
/// Kept for API symmetry with `len` (clippy recommends both together).
|
||||||
/// Unused in code today — callers check `len() == 0` via the HUD.
|
/// Unused in code today — callers check `len() == 0` via the HUD.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -90,6 +126,7 @@ impl TextureCacheHost {
|
|||||||
// dummy and let the pipeline fall back to its own magenta
|
// dummy and let the pipeline fall back to its own magenta
|
||||||
// placeholder. We still create a minimal 1×1 magenta view
|
// placeholder. We still create a minimal 1×1 magenta view
|
||||||
// so `get` returns something bindable.
|
// so `get` returns something bindable.
|
||||||
|
let is_new = !self.entries.contains_key(&key);
|
||||||
let (tex, view) = create_magenta_stub(device, queue);
|
let (tex, view) = create_magenta_stub(device, queue);
|
||||||
let entry = HostTextureEntry {
|
let entry = HostTextureEntry {
|
||||||
texture: tex,
|
texture: tex,
|
||||||
@@ -98,6 +135,7 @@ impl TextureCacheHost {
|
|||||||
};
|
};
|
||||||
self.entries.insert(key, entry);
|
self.entries.insert(key, entry);
|
||||||
self.uploads_total += 1;
|
self.uploads_total += 1;
|
||||||
|
self.record_and_evict(key, is_new);
|
||||||
return self.entries.get(&key).unwrap();
|
return self.entries.get(&key).unwrap();
|
||||||
};
|
};
|
||||||
let texture = device.create_texture(&descriptor);
|
let texture = device.create_texture(&descriptor);
|
||||||
@@ -145,6 +183,7 @@ impl TextureCacheHost {
|
|||||||
} else {
|
} else {
|
||||||
self.uploads_total += 1;
|
self.uploads_total += 1;
|
||||||
}
|
}
|
||||||
|
self.record_and_evict(key, !had_prior);
|
||||||
self.entries.get(&key).unwrap()
|
self.entries.get(&key).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,13 @@ pub struct XenosPipeline {
|
|||||||
pub target_format: wgpu::TextureFormat,
|
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 {
|
impl XenosPipeline {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
device: &wgpu::Device,
|
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 {
|
let tex_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
label: Some("xenos tex bind group layout"),
|
label: Some("xenos tex bind group layout"),
|
||||||
entries: &[
|
entries: &tex_bgl_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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
@@ -437,19 +447,21 @@ impl XenosPipeline {
|
|||||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
..Default::default()
|
..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 {
|
let tex_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some("xenos tex bind group"),
|
label: Some("xenos tex bind group"),
|
||||||
layout: &tex_bgl,
|
layout: &tex_bgl,
|
||||||
entries: &[
|
entries: &tex_bg_entries,
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: wgpu::BindingResource::TextureView(&dummy_view),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: wgpu::BindingResource::Sampler(&dummy_sampler),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
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
|
/// [`TextureCacheHost`]. Pass `None` to revert to the built-in dummy
|
||||||
/// magenta stub.
|
/// magenta stub.
|
||||||
pub fn set_texture_view(&mut self, device: &wgpu::Device, view: Option<&wgpu::TextureView>) {
|
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 {
|
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,
|
layout: &self.tex_bgl,
|
||||||
entries: &[
|
entries: &entries,
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: wgpu::BindingResource::TextureView(bound),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user