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>
376 lines
18 KiB
Rust
376 lines
18 KiB
Rust
//! Per-draw geometry capture for the host UI's faithful-render path.
|
||
//!
|
||
//! The deterministic headless core (`check --gpu-inline`) never touches this
|
||
//! module — it is populated only when a UI bridge is installed and consumed
|
||
//! only by `crates/xenia-ui`. The goal is to hand the UI the *real* guest
|
||
//! geometry behind each `PM4_DRAW_INDX*` packet so it can rasterize the
|
||
//! actual splash vertices instead of synthetic placeholder shapes.
|
||
//!
|
||
//! What the WGSL pipeline needs to reconstruct one draw (see
|
||
//! `shaders/xenos_interp.wgsl` `vs_main` / `interpret_vertex_fetch`):
|
||
//! * the active VS/PS blob keys (already published as assets),
|
||
//! * the primitive type + the host vertex count to issue,
|
||
//! * the raw guest vertex-buffer bytes for the fetched window, and
|
||
//! * the *dword base* of that window so the shader can rebase the absolute
|
||
//! fetch-constant address into the uploaded buffer.
|
||
//!
|
||
//! The hard part is sourcing the vertex window: the VS reads a vertex-fetch
|
||
//! constant (`xe_gpu_vertex_fetch_t`) whose dword-0 carries the absolute
|
||
//! guest dword address. We parse the active VS, find its first vertex fetch,
|
||
//! read that fetch constant out of the register file, then copy a bounded
|
||
//! window of guest memory starting at the fetch base.
|
||
|
||
use xenia_memory::access::MemoryAccess;
|
||
|
||
use crate::draw_state::{IndexSize, IndexSource, PrimitiveType};
|
||
use crate::register_file::RegisterFile;
|
||
|
||
/// Texture-fetch / vertex-fetch constant region base, in register indices.
|
||
/// Each fetch constant is 6 dwords (`xe_gpu_*_fetch_t`).
|
||
const CONST_BASE_FETCH: u32 = 0x4800;
|
||
|
||
/// Upper bound (in dwords) on the vertex window we copy per draw. The splash
|
||
/// UI draws are tiny (3–4 verts × ≤4 dwords); 64 KiB of dwords is generous
|
||
/// slack while bounding the per-frame copy cost and the 16 MiB host buffer.
|
||
const MAX_WINDOW_DWORDS: u32 = 16 * 1024;
|
||
|
||
/// One captured draw, with enough real state for the UI to replay it through
|
||
/// the existing wgpu Xenos pipeline.
|
||
#[derive(Clone, Debug)]
|
||
pub struct DrawCapture {
|
||
/// Monotonic global draw index (matches `GpuStats::draws_seen` at capture).
|
||
pub draw_index: u32,
|
||
/// Xenos primitive-type code (see `SwapInfo::last_draw_prim` encoding).
|
||
pub prim_code: u32,
|
||
/// Host vertex count to issue (post primitive-processor rewrite).
|
||
pub host_vertex_count: u32,
|
||
/// Active VS blob key at draw time (0 = none).
|
||
pub vs_key: u32,
|
||
/// Active PS blob key at draw time (0 = none).
|
||
pub ps_key: u32,
|
||
/// Raw guest dwords of the fetched vertex window (host-endian as stored in
|
||
/// guest memory — the WGSL applies the per-format endian swap). `addr 0`
|
||
/// of this buffer corresponds to guest dword `window_base_dwords`.
|
||
pub vertex_dwords: Vec<u32>,
|
||
/// Guest dword address that maps to index 0 of `vertex_dwords`. The shader
|
||
/// subtracts this from the fetch-constant base to index `vertex_dwords`.
|
||
pub window_base_dwords: u32,
|
||
/// `true` when we successfully resolved a real vertex window. When `false`
|
||
/// the UI falls back to its procedural geometry for this draw (honest:
|
||
/// nothing faked, just "couldn't source real vertices").
|
||
pub has_real_vertices: bool,
|
||
/// iterate-3S: per-draw NDC transform derived from the guest viewport /
|
||
/// clip / VTE registers (mirrors canary `GetHostViewportInfo`). The host VS
|
||
/// converts the guest-VS position to wgpu clip space via
|
||
/// `clip.xy = pos.xy * ndc_scale + ndc_offset * pos.w`. The Y component
|
||
/// already carries the render-target → wgpu Y-flip (negated).
|
||
pub ndc_scale: [f32; 2],
|
||
pub ndc_offset: [f32; 2],
|
||
/// iterate-3T: the decoded texture(s) this draw's active pixel shader
|
||
/// samples, keyed off its real `tfetch` fetch-constant slots (the 3M
|
||
/// decoder makes these decode). Root-#3: the UI uploads + binds EACH entry
|
||
/// to its own texture slot per-draw, so a multi-plane shader (e.g. the
|
||
/// intro video's YUV Y/U/V planes) samples the right texture per fetch.
|
||
/// Empty for flat (no-tfetch) draws. Populated by `gpu_system` after
|
||
/// decode (left empty by `build`).
|
||
///
|
||
/// Each entry is `(slot, key, content_version, bytes)` where `slot` is the
|
||
/// `tfetch` fetch-constant index (0..31) the shader samples this texture
|
||
/// from. iterate-3AD: the `content_version` (from `span_max_version` over
|
||
/// the texel span) lets the UI host texture cache RE-UPLOAD when the guest
|
||
/// fills more of an evolving atlas. The publisher and the 2nd splash logo
|
||
/// share one K8888 surface (base `0x4dbee000`); the 2nd logo's texels are
|
||
/// CPU-written *after* the publisher's first upload. Without the real
|
||
/// version the host cache (which previously pinned `version_when_uploaded
|
||
/// = 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
|
||
/// register file so the host pipeline composites the way the guest
|
||
/// intends (instead of one fixed alpha-blend state). Mirrors the fields
|
||
/// canary feeds into `GetCurrentStateDescription` (D3D12
|
||
/// `pipeline_cache.cc`):
|
||
/// * `blend_control` = `RB_BLENDCONTROL0` (RT0 src/dst factors + op,
|
||
/// color and alpha). The Xbox 360 has no separate "blend enable" bit;
|
||
/// `One,Zero,Add` *is* the opaque case.
|
||
/// * `color_mask` = RT0 nibble of `RB_COLOR_MASK` (per-channel write
|
||
/// enable). When 0, canary forces `One,Zero` (no blend).
|
||
/// * `color_control` = `RB_COLORCONTROL` (alpha-test enable/func).
|
||
/// * `depth_control` = `RB_DEPTHCONTROL` (z-test enable/func/write).
|
||
pub blend_control: u32,
|
||
pub color_mask: u8,
|
||
pub color_control: u32,
|
||
pub depth_control: u32,
|
||
}
|
||
|
||
/// iterate-3S: compute the guest→host NDC XY transform for a draw, mirroring
|
||
/// canary's `draw_util.cc::GetHostViewportInfo` (the XY half). The Xbox 360 VS
|
||
/// emits a clip-space position which the HW then scales/offsets by the viewport
|
||
/// (`PA_CL_VPORT_*`, gated by `PA_CL_VTE_CNTL`) into render-target pixels, OR,
|
||
/// when clipping is disabled (`PA_CL_CLIP_CNTL.clip_disable`), the VS emits
|
||
/// render-target-pixel coordinates directly (the screen-space UI / clear case —
|
||
/// this is what Sylpheed's splash quads do). Either way we must rescale into the
|
||
/// host's [-1,1] clip space and flip Y (render-target Y-down → wgpu Y-up).
|
||
///
|
||
/// Returns `(ndc_scale[2], ndc_offset[2])` such that
|
||
/// `host_clip.xy = guest_pos.xy * ndc_scale + ndc_offset * guest_pos.w`.
|
||
/// The Y entries are pre-negated to flip into wgpu's Y-up clip space.
|
||
pub fn compute_ndc_xy(rf: &RegisterFile) -> ([f32; 2], [f32; 2]) {
|
||
const PA_CL_CLIP_CNTL: u32 = 0x2204;
|
||
const PA_SU_SC_MODE_CNTL: u32 = 0x2205;
|
||
const PA_CL_VTE_CNTL: u32 = 0x2206;
|
||
const PA_SU_VTX_CNTL: u32 = 0x2302;
|
||
const PA_CL_VPORT_XSCALE: u32 = 0x210F;
|
||
const PA_CL_VPORT_XOFFSET: u32 = 0x2110;
|
||
const PA_CL_VPORT_YSCALE: u32 = 0x2111;
|
||
const PA_CL_VPORT_YOFFSET: u32 = 0x2112;
|
||
const PA_SC_WINDOW_OFFSET: u32 = 0x2080;
|
||
const PA_SC_WINDOW_SCISSOR_BR: u32 = 0x2082;
|
||
const RB_SURFACE_INFO: u32 = 0x2000;
|
||
|
||
let clip_cntl = rf.read(PA_CL_CLIP_CNTL);
|
||
let vte = rf.read(PA_CL_VTE_CNTL);
|
||
let su_sc_mode = rf.read(PA_SU_SC_MODE_CNTL);
|
||
let su_vtx = rf.read(PA_SU_VTX_CNTL);
|
||
let fbits = |r: u32| f32::from_bits(rf.read(r));
|
||
|
||
// VTE enable bits (xenos.h PA_CL_VTE_CNTL): bit0 vport_x_scale_ena,
|
||
// bit1 vport_x_offset_ena, bit2 vport_y_scale_ena, bit3 vport_y_offset_ena.
|
||
let scale_x = if vte & (1 << 0) != 0 { fbits(PA_CL_VPORT_XSCALE) } else { 1.0 };
|
||
let off_x = if vte & (1 << 1) != 0 { fbits(PA_CL_VPORT_XOFFSET) } else { 0.0 };
|
||
let scale_y = if vte & (1 << 2) != 0 { fbits(PA_CL_VPORT_YSCALE) } else { 1.0 };
|
||
let off_y = if vte & (1 << 3) != 0 { fbits(PA_CL_VPORT_YOFFSET) } else { 0.0 };
|
||
|
||
// Render-target extent in guest pixels: clamp to the texture max (2048),
|
||
// sourced from the window scissor BR (matches canary `x_max`/`y_max`).
|
||
let br = rf.read(PA_SC_WINDOW_SCISSOR_BR);
|
||
let x_max = ((br & 0x7FFF).max(1)).min(2048) as f32;
|
||
let y_max = (((br >> 16) & 0x7FFF).max(1)).min(2048) as f32;
|
||
let _ = RB_SURFACE_INFO;
|
||
|
||
// Half-pixel + window offsets added in render-target pixels.
|
||
let mut add_x = 0.0f32;
|
||
let mut add_y = 0.0f32;
|
||
if su_sc_mode & (1 << 16) != 0 {
|
||
let wo = rf.read(PA_SC_WINDOW_OFFSET);
|
||
// 15-bit signed each (x: [14:0], y: [30:16]).
|
||
let sx = (((wo & 0x7FFF) << 1) as i32) >> 1;
|
||
let sy = ((((wo >> 16) & 0x7FFF) << 1) as i32) >> 1;
|
||
add_x += sx as f32;
|
||
add_y += sy as f32;
|
||
}
|
||
if su_vtx & 1 == 0 {
|
||
// pix_center == kD3DZero → +0.5 half-pixel offset.
|
||
add_x += 0.5;
|
||
add_y += 0.5;
|
||
}
|
||
|
||
let (s, o);
|
||
if clip_cntl & (1 << 16) != 0 {
|
||
// clip_disable: VS outputs render-target-*pixel* coords (Y-DOWN: pixel
|
||
// y=0 is the top row of the render target). Rescale the whole RT extent
|
||
// to [-1,1] and FLIP Y so pixel-top → wgpu clip-top (canary's
|
||
// huge-host-viewport path; the framebuffer→clip flip is real here).
|
||
let px2ndc_x = 2.0 / x_max;
|
||
let px2ndc_y = 2.0 / y_max;
|
||
let sx = scale_x * px2ndc_x;
|
||
let ox = (off_x - x_max * 0.5 + add_x) * px2ndc_x;
|
||
let sy = scale_y * px2ndc_y;
|
||
let oy = (off_y - y_max * 0.5 + add_y) * px2ndc_y;
|
||
// Flip Y: pixel-Y-down → wgpu clip-Y-up.
|
||
s = [sx, -sy];
|
||
o = [ox, -oy];
|
||
} else {
|
||
// iterate-3AA (DEFECT 1 ROOT): clipping enabled → the VS already emits
|
||
// *clip-space* coordinates (Y-UP: +Y is the top of the screen), exactly
|
||
// the convention the Xbox 360's D3D9 and wgpu BOTH use for clip space
|
||
// (NDC +Y → framebuffer top in each API; the framebuffer Y-direction is
|
||
// an internal viewport detail handled identically by both). A clip-space
|
||
// position is therefore portable to wgpu with NO Y-flip. The previous
|
||
// code unconditionally negated Y (the same flip the screen-space pixel
|
||
// path needs), which mirrored the publisher logo vertically: its quad is
|
||
// centered (±0.085 around 0) so the *position* stayed centered, but the
|
||
// negation swapped top↔bottom vertices while the texture V was unchanged
|
||
// → the sampled sub-rect (UV v 0.001→0.090) read bottom-up → "SQUARE
|
||
// ENIX" rendered upside down in place. Measured (readback): the red dots
|
||
// sit at 43% from the texture top but rendered at 58% from the top
|
||
// (= a clean vertical mirror); removing the flip restores them to 43%.
|
||
// Identity XY (no flip) maps guest clip-Y-up straight to wgpu clip-Y-up.
|
||
s = [1.0, 1.0];
|
||
o = [0.0, 0.0];
|
||
return (s, o);
|
||
}
|
||
(s, o)
|
||
}
|
||
|
||
/// Encode a [`PrimitiveType`] as the raw Xenos code used across the bridge.
|
||
pub fn prim_code(p: PrimitiveType) -> u32 {
|
||
match p {
|
||
PrimitiveType::None => 0,
|
||
PrimitiveType::PointList => 1,
|
||
PrimitiveType::LineList => 2,
|
||
PrimitiveType::LineStrip => 3,
|
||
PrimitiveType::TriangleList => 4,
|
||
PrimitiveType::TriangleFan => 5,
|
||
PrimitiveType::TriangleStrip => 6,
|
||
PrimitiveType::RectangleList => 8,
|
||
PrimitiveType::QuadList => 13,
|
||
PrimitiveType::Unknown(x) => x as u32,
|
||
}
|
||
}
|
||
|
||
/// Resolve the first vertex-fetch window referenced by the parsed VS.
|
||
///
|
||
/// Walks the VS instruction stream for the first `vfetch` (mini) instruction,
|
||
/// reads its fetch constant from `rf`, and copies a bounded window of guest
|
||
/// memory starting at the fetch base. Returns `(dwords, window_base_dwords)`
|
||
/// or `None` if the VS has no vertex fetch or the constant is malformed.
|
||
fn resolve_vertex_window(
|
||
parsed_vs: &crate::ucode::ParsedShader,
|
||
rf: &RegisterFile,
|
||
mem: &dyn MemoryAccess,
|
||
) -> Option<(Vec<u32>, u32)> {
|
||
// iterate-3W (GPUBUG-109): the instruction block packs ALU and fetch
|
||
// instructions identically (96 bits / 3 dwords each); ONLY the owning
|
||
// `Exec` control-flow clause's `sequence` bitmap (2 bits per instruction,
|
||
// bit[2*i]=fetch/ALU) tells them apart. The previous blind triple-walk
|
||
// decoded ALU triples as fetches → garbage fetch-constant indices and a
|
||
// bogus `type==3` guard, never reaching the real vertex fetch. Walk the CF
|
||
// exec clauses exactly as the translator does (`translator.rs::emit_exec`)
|
||
// and take the FIRST sequence-flagged *vertex* fetch.
|
||
let instrs = &parsed_vs.instructions;
|
||
let mut const_off: Option<u32> = None;
|
||
'clauses: for clause in &parsed_vs.cf {
|
||
let crate::ucode::control_flow::ControlFlowInstruction::Exec {
|
||
address,
|
||
count,
|
||
sequence,
|
||
..
|
||
} = *clause
|
||
else {
|
||
continue;
|
||
};
|
||
for i in 0..(count as usize) {
|
||
// bit[2*i] of the sequence bitmap: 1 = fetch, 0 = ALU.
|
||
if (sequence >> (i * 2)) & 1 == 0 {
|
||
continue;
|
||
}
|
||
let base = (address as usize + i) * 3;
|
||
if base + 2 >= instrs.len() {
|
||
break;
|
||
}
|
||
if let crate::ucode::fetch::FetchInstruction::Vertex(vf) =
|
||
crate::ucode::fetch::decode_fetch([instrs[base], instrs[base + 1], instrs[base + 2]])
|
||
{
|
||
const_off = Some(vf.const_reg_offset());
|
||
break 'clauses;
|
||
}
|
||
}
|
||
}
|
||
// iterate-3X (GPUBUG-110): vertex fetch constants are addressed by
|
||
// `const_index * 3 + const_index_sel` (canary `ucode.h:700` —
|
||
// `VertexFetchInstruction::fetch_constant_index`), NOT by `const_index`
|
||
// alone. The register region packs 3 two-dword vertex-fetch constants per
|
||
// 6-dword group, so the constant lives at
|
||
// `0x4800 + const_index*6 + const_index_sel*2`. The previous decode dropped
|
||
// `const_index_sel` and read sub-slot 0 (`fc*6`), which for the publisher
|
||
// logo (`const_index=31, sel=2`) held `0x00000001` (an unused slot) instead
|
||
// of the real vertex-buffer base at sub-slot 2 (`0x48BE`). That made
|
||
// `has_real_vertices=false` → the logo fell to the procedural fullscreen
|
||
// magenta fallback. (Refutes iterate-3W's "geometry is auto-generated from
|
||
// vertex_id" — measured: the real fetch constant is a 4-vertex QuadList
|
||
// buffer at `0x0adf60f0`.)
|
||
let const_reg = CONST_BASE_FETCH + const_off?;
|
||
let dword0 = rf.read(const_reg);
|
||
let dword1 = rf.read(const_reg + 1);
|
||
// address:30 at bits[31:2] of dword0 (in bytes once masked). The fetch
|
||
// constant carries a guest *physical* dword address — canary reads the
|
||
// vertex buffer via `Memory::TranslatePhysical(fetch.address * 4)`
|
||
// (`draw_util.cc:961`). On the Xbox 360 the physical range is mirrored at
|
||
// several virtual windows; ours only maps the cached-physical window at
|
||
// `0x4000_0000` (`gpu_system::physical_to_backing`). Reading the bare low
|
||
// address (`0x0adf_xxxx`) hits an unmapped VA and returns zeros, so rebase
|
||
// a low physical base onto the mapped `0x4000_0000` alias when the raw VA
|
||
// is not itself mapped. `window_base_dwords` keeps the *original* base so
|
||
// the shader's rebase against the (unmodified) fetch-constant address still
|
||
// indexes the uploaded window correctly.
|
||
let base_bytes = dword0 & 0xFFFF_FFFC;
|
||
if base_bytes == 0 {
|
||
return None;
|
||
}
|
||
let read_base = if mem.translate(base_bytes).is_some() {
|
||
base_bytes
|
||
} else if base_bytes < 0x2000_0000 && mem.translate(base_bytes | 0x4000_0000).is_some() {
|
||
base_bytes | 0x4000_0000
|
||
} else {
|
||
base_bytes
|
||
};
|
||
// size:24 at bits[25:2] of dword1, in dwords. Clamp to our window cap.
|
||
let size_dwords = ((dword1 >> 2) & 0x00FF_FFFF).clamp(1, MAX_WINDOW_DWORDS);
|
||
let window_base_dwords = base_bytes >> 2;
|
||
let mut dwords = Vec::with_capacity(size_dwords as usize);
|
||
for i in 0..size_dwords {
|
||
let addr = read_base.wrapping_add(i * 4);
|
||
if addr < read_base {
|
||
break; // wrap guard
|
||
}
|
||
// `read_u32` composes big-endian bytes into the u32 value; the WGSL's
|
||
// `gpu_swap` expects the *raw little-endian dword* as it sits in guest
|
||
// memory, so undo the BE composition with `swap_bytes`.
|
||
dwords.push(mem.read_u32(addr).swap_bytes());
|
||
}
|
||
if dwords.is_empty() {
|
||
return None;
|
||
}
|
||
Some((dwords, window_base_dwords))
|
||
}
|
||
|
||
/// Build a [`DrawCapture`] for one draw. Best-effort: when the vertex window
|
||
/// can't be resolved, `has_real_vertices` is `false` and the UI falls back to
|
||
/// procedural geometry (never fabricated pixels).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn build(
|
||
draw_index: u32,
|
||
primitive: PrimitiveType,
|
||
host_vertex_count: u32,
|
||
_index_source: IndexSource,
|
||
_index_size: IndexSize,
|
||
vs_key: u32,
|
||
ps_key: u32,
|
||
parsed_vs: Option<&crate::ucode::ParsedShader>,
|
||
rf: &RegisterFile,
|
||
mem: &dyn MemoryAccess,
|
||
) -> DrawCapture {
|
||
let (vertex_dwords, window_base_dwords, has_real) = match parsed_vs
|
||
.and_then(|vs| resolve_vertex_window(vs, rf, mem))
|
||
{
|
||
Some((d, base)) => (d, base, true),
|
||
None => (Vec::new(), 0, false),
|
||
};
|
||
let (ndc_scale, ndc_offset) = compute_ndc_xy(rf);
|
||
// iterate-3Y: capture RT0 color/blend/depth render state. Registers per
|
||
// canary `registers.h`: RB_BLENDCONTROL0=0x2201, RB_COLOR_MASK=0x2104
|
||
// (RT0 = bits[3:0]), RB_COLORCONTROL=0x2202, RB_DEPTHCONTROL=0x2200.
|
||
const RB_BLENDCONTROL_0: u32 = 0x2201;
|
||
const RB_COLOR_MASK: u32 = 0x2104;
|
||
const RB_COLORCONTROL: u32 = 0x2202;
|
||
const RB_DEPTHCONTROL: u32 = 0x2200;
|
||
DrawCapture {
|
||
draw_index,
|
||
prim_code: prim_code(primitive),
|
||
host_vertex_count,
|
||
vs_key,
|
||
ps_key,
|
||
vertex_dwords,
|
||
window_base_dwords,
|
||
has_real_vertices: has_real,
|
||
ndc_scale,
|
||
ndc_offset,
|
||
textures: Vec::new(),
|
||
blend_control: rf.read(RB_BLENDCONTROL_0),
|
||
color_mask: (rf.read(RB_COLOR_MASK) & 0xF) as u8,
|
||
color_control: rf.read(RB_COLORCONTROL),
|
||
depth_control: rf.read(RB_DEPTHCONTROL),
|
||
}
|
||
}
|