xenia-gpu: end-to-end Xenos pipeline (PM4, ucode, EDRAM, resolve)

First real GPU implementation. Ring/PM4 frontend (ring_view,
ring_drain, pm4) drains the command processor; gpu_system owns the
threaded backend (DrainFence RPC + parker/fence helpers from M1) and
the MMIO-mapped register block (mmio_region).

Xenos shader frontend: ucode/{alu,control_flow,fetch,mod}.rs decode
the Xbox 360 microcode, translator.rs lowers it onto the WGSL
xenos_interp interpreter shader (shaders/xenos_interp.wgsl).
shader_metrics.rs counts decode/translate work.

Render state: draw_state, primitive, render_target_cache,
texture_cache, tiled_address (Xenos's swizzled tiled-memory layout),
xenos_constants (register field constants), edram (the 10 MiB EDRAM
model with MSAA), and resolve.rs (TILE_FLUSH copy-out — clear-resolve
plus bitwise-equivalent 32 bpp + 64 bpp paths landed). handle.rs
owns the typed GPU-resource handles the kernel hands out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-01 16:29:38 +02:00
parent 5f0d6487ea
commit 79eb52c378
24 changed files with 10984 additions and 18 deletions

View File

@@ -0,0 +1,557 @@
//! Xenos → WGSL direct translator (P7).
//!
//! Replaces the runtime uber-shader interpreter (P3b/P3c) for shaders whose
//! feature set we cover. Emits a *standalone* WGSL module per shader
//! instead of walking a ucode buffer at draw time — pipeline compilation
//! happens once, then every subsequent dispatch is a direct `draw()`.
//!
//! The translator is deliberately narrow: when it encounters an opcode /
//! fetch format / CF shape it doesn't know, it returns [`None`] and the
//! caller falls back to the interpreter. This keeps the op-coverage work
//! incremental — future commits can add one opcode at a time without
//! invalidating the scaffolding.
//!
//! Current coverage (v1):
//! * Linear CF: `Exec`/`ExecEnd`, `Alloc`, `Exit`. No loops / branches /
//! calls / predicate-gated clauses.
//! * ALU vector: `ADD`, `MUL`, `MAX`, `MIN`, `MAD`, `DP4`, `DP3`,
//! `DP2_ADD`, `SEQ`, `SGT`, `SGE`, `SNE`, `FRC`, `FLOOR`.
//! * ALU scalar: `ADDS`, `MULS`, `MAXS`, `MINS`, `RCP`, `RETAIN_PREV`.
//! * Vertex fetch: `R32G32B32A32_FLOAT` only.
//! * Texture fetch: 2D via the single `@group(1)` slot (same one P5/M6
//! binds).
//! * Exports: VS writes position + interpolator 0 (color); PS writes
//! color0.
//!
//! When a shader exceeds this subset, [`translate`] returns `None` and
//! `gpu.shader.translate_reject{reason}` is bumped by the caller.
use crate::ucode::alu::{decode_alu, sop, vop, AluInstruction};
use crate::ucode::control_flow::{AllocKind, ControlFlowInstruction};
use crate::ucode::fetch::{decode_fetch, FetchInstruction};
use crate::ucode::ParsedShader;
/// Shader stage we're emitting for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Vertex,
Pixel,
}
/// Success or refusal from the translator. On refusal, the caller falls
/// back to the runtime uber-shader interpreter.
#[derive(Debug)]
pub enum Translation {
/// The emitted WGSL body for *this stage only*. Both VS + PS get
/// wrapped into one module via [`combine_stages`].
Ok(String),
/// Translator saw an op/pattern it doesn't handle; fallback.
Reject(&'static str),
}
/// Full WGSL module for a (VS, PS) pair ready to hand to
/// `wgpu::Device::create_shader_module`. Shares the header across the two
/// bodies so bindings, struct declarations, and helpers aren't duplicated.
pub fn combine_stages(vs_body: &str, ps_body: &str) -> String {
let mut out = String::with_capacity(4096 + vs_body.len() + ps_body.len());
out.push_str(MODULE_HEADER);
out.push_str(vs_body);
out.push_str(ps_body);
out
}
/// Translate a single shader stage. Returns `None` on any unsupported
/// feature with a short reason string that the caller plumbs into the
/// `gpu.shader.translate_reject{reason}` metric.
pub fn translate(parsed: &ParsedShader, stage: Stage) -> Translation {
let mut ctx = EmitCtx::new(stage);
// Emit the stage entry function body.
if let Err(reason) = ctx.emit_stage_body(parsed) {
return Translation::Reject(reason);
}
Translation::Ok(ctx.finish())
}
/// Reject reasons; kept as static &'str for zero-alloc metrics.
pub mod reject {
pub const VEC_OP_UNSUPPORTED: &str = "vec_op_unsupported";
pub const SCL_OP_UNSUPPORTED: &str = "scl_op_unsupported";
pub const CF_LOOP: &str = "cf_loop";
pub const CF_COND: &str = "cf_cond";
pub const CF_CALL: &str = "cf_call";
pub const CF_UNKNOWN: &str = "cf_unknown";
pub const VFETCH_FMT: &str = "vfetch_fmt";
pub const TFETCH_NON2D: &str = "tfetch_non2d";
pub const INSTR_OOB: &str = "instr_oob";
}
/// Shader-module preamble (bindings, helpers, struct defs). The bindings
/// mirror the xenos pipeline's `@group(0)` + `@group(1)` layout from P5/M6
/// so we can use **the same bind-group slots** — only the pipeline object
/// differs between interpreter mode and translator mode.
const MODULE_HEADER: &str = r#"
struct XenosDrawConstants {
draw_index: u32,
vertex_count: u32,
prim_kind: u32,
_pad: u32,
};
struct XenosConstants {
alu: array<vec4<f32>, 512>,
fetch: array<u32, 256>,
bool_consts: array<u32, 8>,
loop_consts: array<u32, 32>,
};
@group(0) @binding(0) var<uniform> draw_ctx : XenosDrawConstants;
@group(0) @binding(1) var<storage, read> xenos_consts : XenosConstants;
@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>;
@group(1) @binding(0) var xenos_tex : texture_2d<f32>;
@group(1) @binding(1) var xenos_samp : sampler;
struct VsOut {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
};
struct FsOut {
@location(0) color0: vec4<f32>,
};
// Helper: reciprocal guarded against divide-by-zero.
fn xe_rcp(x: f32) -> f32 {
return select(0.0, 1.0 / x, x != 0.0);
}
"#;
struct EmitCtx {
stage: Stage,
out: String,
indent: usize,
}
impl EmitCtx {
fn new(stage: Stage) -> Self {
Self {
stage,
out: String::with_capacity(2048),
indent: 0,
}
}
fn finish(self) -> String {
self.out
}
fn push(&mut self, s: &str) {
for _ in 0..self.indent {
self.out.push_str(" ");
}
self.out.push_str(s);
self.out.push('\n');
}
fn emit_stage_body(&mut self, parsed: &ParsedShader) -> Result<(), &'static str> {
// Entry function + struct header.
match self.stage {
Stage::Vertex => {
self.push("@vertex");
self.push("fn vs_main(@builtin(vertex_index) vidx: u32) -> VsOut {");
}
Stage::Pixel => {
self.push("@fragment");
self.push("fn fs_main(in: VsOut) -> FsOut {");
}
}
self.indent = 1;
// Register file + ps chain + export slots. All local `var`s so each
// invocation gets its own state; translator-emitted code doesn't
// need `var<private>` because we don't share across function calls.
self.push("var r: array<vec4<f32>, 128>;");
self.push("for (var i = 0u; i < 128u; i = i + 1u) { r[i] = vec4<f32>(0.0); }");
self.push("var ps: f32 = 0.0;");
match self.stage {
Stage::Vertex => {
// Seed r0 with vertex index for simple shaders that read it.
self.push("r[0] = vec4<f32>(f32(vidx), 0.0, 0.0, 1.0);");
// Synthetic export slots — match the interpreter's layout so
// the fallback path and translator path produce the same
// visual output on shaders both support.
self.push("var opos: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 1.0);");
self.push("var ocolor: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);");
}
Stage::Pixel => {
// Seed r0.xy with interpolated color lane so trivial shaders
// that read r0 still produce something.
self.push("r[0] = in.color;");
self.push("var ocolor0: vec4<f32> = in.color;");
}
}
let mut current_alloc = AllocKind::Other;
for clause in &parsed.cf {
match clause {
ControlFlowInstruction::Exec {
address,
count,
sequence,
is_end,
predicated,
..
} => {
if *predicated {
return Err(reject::CF_COND);
}
self.emit_exec(parsed, *address, *count, *sequence, current_alloc)?;
if *is_end {
break;
}
}
ControlFlowInstruction::Alloc { kind, .. } => {
current_alloc = *kind;
}
ControlFlowInstruction::Exit => break,
ControlFlowInstruction::LoopStart { .. }
| ControlFlowInstruction::LoopEnd { .. } => return Err(reject::CF_LOOP),
ControlFlowInstruction::CondJmp { .. } => return Err(reject::CF_COND),
ControlFlowInstruction::CondCall { .. } | ControlFlowInstruction::Return => {
return Err(reject::CF_CALL);
}
ControlFlowInstruction::Unknown { .. } => return Err(reject::CF_UNKNOWN),
}
}
match self.stage {
Stage::Vertex => {
self.push("var out: VsOut;");
self.push("out.position = opos;");
self.push("out.color = ocolor;");
self.push("return out;");
}
Stage::Pixel => {
self.push("var out: FsOut;");
self.push("out.color0 = ocolor0;");
self.push("return out;");
}
}
self.indent = 0;
self.push("}");
Ok(())
}
fn emit_exec(
&mut self,
parsed: &ParsedShader,
address: u32,
count: u32,
sequence: u32,
current_alloc: AllocKind,
) -> Result<(), &'static str> {
for i in 0..(count as usize) {
let triple_idx = address as usize + i;
let base = triple_idx * 3;
if base + 2 >= parsed.instructions.len() {
return Err(reject::INSTR_OOB);
}
let words = [
parsed.instructions[base],
parsed.instructions[base + 1],
parsed.instructions[base + 2],
];
let is_fetch = ((sequence >> (i * 2 + 1)) & 1) != 0;
if is_fetch {
match decode_fetch(words) {
FetchInstruction::Vertex(vf) => self.emit_vfetch(&vf)?,
FetchInstruction::Texture(tf) => {
if tf.dimension != 1 {
return Err(reject::TFETCH_NON2D);
}
self.emit_tfetch(&tf);
}
FetchInstruction::Unknown { .. } => return Err(reject::VFETCH_FMT),
}
} else {
let alu = decode_alu(words);
self.emit_alu(&alu, current_alloc)?;
}
}
Ok(())
}
fn emit_alu(
&mut self,
alu: &AluInstruction,
current_alloc: AllocKind,
) -> Result<(), &'static str> {
let a = format!("r[{}u]", alu.src_a & 0x7F);
let b = format!("r[{}u]", alu.src_b & 0x7F);
let c = format!("r[{}u]", alu.src_c & 0x7F);
// Vector pipe.
if alu.vector_write_mask != 0 {
let expr = vector_expr(alu.vector_opcode, &a, &b, &c)
.ok_or(reject::VEC_OP_UNSUPPORTED)?;
let dst_reg = alu.vector_dest & 0x7F;
if alu.vector_dest_is_export {
self.emit_export(dst_reg, current_alloc, &expr, alu.vector_write_mask);
} else {
self.emit_masked_write(&format!("r[{dst_reg}u]"), &expr, alu.vector_write_mask);
}
}
// 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
// `scalar_src_is_ps` selector.
let scl_src_a = if alu.scalar_src_is_ps {
"ps".to_string()
} else {
format!("{}.x", a)
};
let scl_src_b = format!("{}.x", b);
let expr = scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps")
.ok_or(reject::SCL_OP_UNSUPPORTED)?;
self.push(&format!("ps = {expr};"));
if alu.scalar_write_mask != 0 {
let v = "vec4<f32>(ps, ps, ps, ps)";
let dst_reg = alu.scalar_dest & 0x7F;
self.emit_masked_write(&format!("r[{dst_reg}u]"), v, alu.scalar_write_mask);
}
Ok(())
}
fn emit_masked_write(&mut self, lhs: &str, rhs: &str, mask: u8) {
if mask == 0xF {
self.push(&format!("{lhs} = {rhs};"));
return;
}
self.push(&"{".to_string());
self.indent += 1;
self.push(&format!("let _prev = {lhs};"));
self.push(&format!("let _new = {rhs};"));
let mut components = Vec::new();
let letters = ['x', 'y', 'z', 'w'];
for (i, c) in letters.iter().enumerate() {
if (mask >> i) & 1 == 1 {
components.push(format!("_new.{c}"));
} else {
components.push(format!("_prev.{c}"));
}
}
self.push(&format!(
"{lhs} = vec4<f32>({}, {}, {}, {});",
components[0], components[1], components[2], components[3]
));
self.indent -= 1;
self.push("}");
}
fn emit_export(&mut self, dst_reg: u8, alloc: AllocKind, expr: &str, mask: u8) {
// Xenos's export "register" indexing within an alloc range is
// normally (alloc_base + offset). Since our CF stream doesn't
// carry per-export slot offsets cleanly, use `alloc` to pick the
// target.
let lhs = match (self.stage, alloc) {
(Stage::Vertex, AllocKind::Position) => "opos",
(Stage::Vertex, AllocKind::Interpolators) => "ocolor",
(Stage::Vertex, AllocKind::Colors) => "ocolor",
(Stage::Vertex, _) => "ocolor", // fall through — any other alloc
(Stage::Pixel, AllocKind::Colors) => "ocolor0",
(Stage::Pixel, _) => "ocolor0",
};
let _ = dst_reg; // per-slot export indexing reserved for a richer v2
self.emit_masked_write(lhs, expr, mask);
}
fn emit_vfetch(&mut self, vf: &crate::ucode::fetch::VertexFetch) -> Result<(), &'static str> {
// v1: treat all vertex fetches as R32G32B32A32_FLOAT, stride = 4
// dwords. Matches the interpreter's MVP semantics; unlocks more
// formats alongside the CPU texture cache's format expansion.
let fetch_const = (vf.raw[0] >> 5) & 0x1F;
let src_reg = vf.src_register & 0x7F;
let dst_reg = vf.dest_register & 0x7F;
self.push(&format!(
"{{ let fc0 = xenos_consts.fetch[{}u]; \
let base = (fc0 & 0xFFFFFFFCu) >> 2u; \
let vidx = u32(r[{src_reg}u].x); \
let addr = base + vidx * 4u; \
let n = arrayLength(&vertex_buffer); \
if (addr + 3u < n) {{ \
r[{dst_reg}u] = vec4<f32>( \
bitcast<f32>(vertex_buffer[addr + 0u]), \
bitcast<f32>(vertex_buffer[addr + 1u]), \
bitcast<f32>(vertex_buffer[addr + 2u]), \
bitcast<f32>(vertex_buffer[addr + 3u])); \
}} }}",
fetch_const * 2,
));
Ok(())
}
fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) {
// v1: sample the single bound texture; UV = r[src].xy. P5's cache
// publishes the `fetch_const=0` texture into `@group(1)`; slot
// mismatch is a silent magenta for now.
let src_reg = tf.src_register & 0x7F;
let dst_reg = tf.dest_register & 0x7F;
self.push(&format!(
"r[{dst_reg}u] = textureSampleLevel(xenos_tex, xenos_samp, r[{src_reg}u].xy, 0.0);"
));
}
}
fn vector_expr(op: u8, a: &str, b: &str, c: &str) -> Option<String> {
let s = match op {
vop::ADD => format!("({a} + {b})"),
vop::MUL => format!("({a} * {b})"),
vop::MAX => format!("max({a}, {b})"),
vop::MIN => format!("min({a}, {b})"),
vop::MAD => format!("({a} * {b} + {c})"),
vop::DOT4 => format!("vec4<f32>(dot({a}, {b}))"),
vop::DOT3 => format!("vec4<f32>(dot({a}.xyz, {b}.xyz))"),
vop::DOT2_ADD => format!(
"vec4<f32>({a}.x * {b}.x + {a}.y * {b}.y + {c}.x)"
),
vop::SEQ => format!(
"vec4<f32>(select(0.0,1.0,{a}.x=={b}.x), select(0.0,1.0,{a}.y=={b}.y), select(0.0,1.0,{a}.z=={b}.z), select(0.0,1.0,{a}.w=={b}.w))"
),
vop::SGT => format!(
"vec4<f32>(select(0.0,1.0,{a}.x>{b}.x), select(0.0,1.0,{a}.y>{b}.y), select(0.0,1.0,{a}.z>{b}.z), select(0.0,1.0,{a}.w>{b}.w))"
),
vop::SGE => format!(
"vec4<f32>(select(0.0,1.0,{a}.x>={b}.x), select(0.0,1.0,{a}.y>={b}.y), select(0.0,1.0,{a}.z>={b}.z), select(0.0,1.0,{a}.w>={b}.w))"
),
vop::SNE => format!(
"vec4<f32>(select(0.0,1.0,{a}.x!={b}.x), select(0.0,1.0,{a}.y!={b}.y), select(0.0,1.0,{a}.z!={b}.z), select(0.0,1.0,{a}.w!={b}.w))"
),
vop::FRC => format!("fract({a})"),
vop::FLOOR => format!("floor({a})"),
_ => return None,
};
Some(s)
}
fn scalar_expr(op: u8, a: &str, b: &str, prev: &str) -> Option<String> {
let s = match op {
sop::ADDS => format!("({a} + {b})"),
sop::ADDS_PREV => format!("({a} + {prev})"),
sop::MULS => format!("({a} * {b})"),
sop::MULS_PREV => format!("({a} * {prev})"),
sop::MAXS => format!("max({a}, {b})"),
sop::MINS => format!("min({a}, {b})"),
sop::RCP => format!("xe_rcp({a})"),
sop::RETAIN_PREV => prev.to_string(),
_ => return None,
};
Some(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ucode::alu::{sop, vop};
use crate::ucode::control_flow::ControlFlowInstruction;
fn synthetic_trivial_shader() -> ParsedShader {
// Single Exec clause: ALU add r0 = r0 + r0; scalar_op = RETAIN_PREV
// with full write-mask on vector, zero on scalar. Alloc(Position)
// precedes so the ALU's export (if it were one) would target oPos.
let w2 = (vop::ADD as u32)
| ((sop::RETAIN_PREV as u32) << 6)
| (0xF << 12) // vector_write_mask
| (0u32 << 16); // vector_dest = 0
ParsedShader {
cf: vec![
ControlFlowInstruction::Alloc {
size: 1,
kind: AllocKind::Position,
},
ControlFlowInstruction::Exec {
address: 0,
count: 1,
sequence: 0,
is_end: true,
predicated: false,
predicate_condition: false,
},
],
instructions: vec![0, 0, w2],
}
}
#[test]
fn trivial_shader_translates() {
let shader = synthetic_trivial_shader();
match translate(&shader, Stage::Vertex) {
Translation::Ok(body) => {
assert!(body.contains("fn vs_main"));
assert!(body.contains("r[0u] = (r[0u] + r[0u]);"));
assert!(body.contains("return out;"));
}
Translation::Reject(r) => panic!("rejected: {r}"),
}
}
#[test]
fn combined_module_parses_as_wgsl() {
let shader = synthetic_trivial_shader();
let vs = match translate(&shader, Stage::Vertex) {
Translation::Ok(body) => body,
Translation::Reject(r) => panic!("VS rejected: {r}"),
};
let ps = match translate(&shader, Stage::Pixel) {
Translation::Ok(body) => body,
Translation::Reject(r) => panic!("PS rejected: {r}"),
};
let module = combine_stages(&vs, &ps);
// naga is pinned as a dev-dep in this crate; if this fails the
// translator is emitting invalid WGSL.
match naga::front::wgsl::parse_str(&module) {
Ok(_) => {}
Err(e) => panic!(
"emitted WGSL failed to parse:\n{}\n--- module ---\n{}",
e, module
),
}
}
#[test]
fn loop_clause_rejected() {
let shader = ParsedShader {
cf: vec![ControlFlowInstruction::LoopStart {
address: 0,
loop_id: 0,
}],
instructions: vec![],
};
assert!(matches!(
translate(&shader, Stage::Vertex),
Translation::Reject(reject::CF_LOOP)
));
}
#[test]
fn unsupported_op_rejected() {
let w2 = (29u32) // VOP_MAX_A, not in v1 subset
| ((sop::RETAIN_PREV as u32) << 6)
| (0xF << 12);
let shader = ParsedShader {
cf: vec![ControlFlowInstruction::Exec {
address: 0,
count: 1,
sequence: 0,
is_end: true,
predicated: false,
predicate_condition: false,
}],
instructions: vec![0, 0, w2],
};
assert!(matches!(
translate(&shader, Stage::Vertex),
Translation::Reject(reject::VEC_OP_UNSUPPORTED)
));
}
}