Compare commits
2 Commits
iterate-2X
...
iterate-3M
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bb4355e3d | ||
|
|
3f5d5cf5f7 |
@@ -6,5 +6,5 @@
|
|||||||
"swaps": 147,
|
"swaps": 147,
|
||||||
"unique_render_targets": 2,
|
"unique_render_targets": 2,
|
||||||
"shader_blobs_live": 6,
|
"shader_blobs_live": 6,
|
||||||
"texture_cache_entries": 0
|
"texture_cache_entries": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ pub fn emit_for(parsed: &ParsedShader, stage: &'static str) {
|
|||||||
parsed.instructions[base + 1],
|
parsed.instructions[base + 1],
|
||||||
parsed.instructions[base + 2],
|
parsed.instructions[base + 2],
|
||||||
];
|
];
|
||||||
// sequence bit layout: 2 bits per triple, hi bit = is-fetch.
|
// sequence: 2 bits per instruction — bit[0]=fetch(1)/ALU(0),
|
||||||
let is_fetch = ((sequence >> (i * 2 + 1)) & 1) != 0;
|
// bit[1]=serialize (Xenos `ucode.h:226`).
|
||||||
|
let is_fetch = ((sequence >> (i * 2)) & 1) != 0;
|
||||||
if is_fetch {
|
if is_fetch {
|
||||||
match decode_fetch(words) {
|
match decode_fetch(words) {
|
||||||
FetchInstruction::Vertex(_) => vfetch_count += 1,
|
FetchInstruction::Vertex(_) => vfetch_count += 1,
|
||||||
@@ -196,8 +197,9 @@ pub fn tfetch_slots(parsed: &ParsedShader) -> Vec<u8> {
|
|||||||
if base + 2 >= parsed.instructions.len() {
|
if base + 2 >= parsed.instructions.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// sequence bit layout: 2 bits per triple, hi bit = is-fetch.
|
// sequence: 2 bits per instruction — bit[0]=fetch(1)/ALU(0),
|
||||||
let is_fetch = ((sequence >> (i * 2 + 1)) & 1) != 0;
|
// bit[1]=serialize (Xenos `ucode.h:226`).
|
||||||
|
let is_fetch = ((sequence >> (i * 2)) & 1) != 0;
|
||||||
if !is_fetch {
|
if !is_fetch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -345,17 +347,17 @@ mod tests {
|
|||||||
/// fetch (and dedup), and return empty for a flat ALU-only shader.
|
/// fetch (and dedup), and return empty for a flat ALU-only shader.
|
||||||
#[test]
|
#[test]
|
||||||
fn tfetch_slots_extracts_texture_fetch_constants() {
|
fn tfetch_slots_extracts_texture_fetch_constants() {
|
||||||
// word0: opcode TEXTURE_FETCH (0x01) in low 5 bits, fetch_const=3 in
|
// word0: opcode TEXTURE_FETCH (0x01) in low 5 bits, const_index=3 in
|
||||||
// bits[9:5] → 0x01 | (3 << 5) = 0x61.
|
// bits[24:20] (Xenos `ucode.h:844`) → 0x01 | (3 << 20).
|
||||||
let tfetch_w0: u32 = 0x01 | (3u32 << 5);
|
let tfetch_w0: u32 = 0x01 | (3u32 << 20);
|
||||||
let shader = ParsedShader {
|
let shader = ParsedShader {
|
||||||
cf: vec![
|
cf: vec![
|
||||||
ControlFlowInstruction::Exec {
|
ControlFlowInstruction::Exec {
|
||||||
address: 0,
|
address: 0,
|
||||||
count: 2,
|
count: 2,
|
||||||
// triple 0 is a fetch (hi bit of its 2-bit field set),
|
// instruction 0 is a fetch (bit[0] of its 2-bit field set),
|
||||||
// triple 1 is ALU. is_fetch = (sequence >> (i*2+1)) & 1.
|
// instruction 1 is ALU. is_fetch = (sequence >> (i*2)) & 1.
|
||||||
sequence: 0b00_10,
|
sequence: 0b00_01,
|
||||||
is_end: false,
|
is_end: false,
|
||||||
predicated: false,
|
predicated: false,
|
||||||
predicate_condition: false,
|
predicate_condition: false,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const CF_KIND_LOOP_END: u32 = 5u;
|
|||||||
const CF_KIND_COND_JMP: u32 = 6u;
|
const CF_KIND_COND_JMP: u32 = 6u;
|
||||||
const CF_KIND_COND_CALL: u32 = 7u;
|
const CF_KIND_COND_CALL: u32 = 7u;
|
||||||
const CF_KIND_RETURN: u32 = 8u;
|
const CF_KIND_RETURN: u32 = 8u;
|
||||||
|
const CF_KIND_NOP: u32 = 9u;
|
||||||
const CF_KIND_UNKNOWN: u32 = 15u;
|
const CF_KIND_UNKNOWN: u32 = 15u;
|
||||||
|
|
||||||
// ── Alloc-kind codes (mirrors `xenia_gpu::ucode::cf_alloc_kind`). ──────
|
// ── Alloc-kind codes (mirrors `xenia_gpu::ucode::cf_alloc_kind`). ──────
|
||||||
@@ -628,8 +629,8 @@ const VFMT_32_32_32_FLOAT: u32 = 57u;
|
|||||||
// layout in `ucode.h:690`):
|
// layout in `ucode.h:690`):
|
||||||
// w0 [4:0] opcode
|
// w0 [4:0] opcode
|
||||||
// w0 [10:5] src_reg[5:0]
|
// w0 [10:5] src_reg[5:0]
|
||||||
// w0 [17:11] dst_reg[6:0] + must-be-one
|
// w0 [17:12] dst_reg[5:0]
|
||||||
// w0 [21:17] const_index[4:0], [23:22] const_index_sel[1:0]
|
// w0 [24:20] const_index[4:0], [26:25] const_index_sel[1:0]
|
||||||
// w1 [21:16] format[5:0]
|
// w1 [21:16] format[5:0]
|
||||||
// w2 [7:0] stride (in dwords)
|
// w2 [7:0] stride (in dwords)
|
||||||
// w2 [30:8] offset (signed, in dwords)
|
// w2 [30:8] offset (signed, in dwords)
|
||||||
@@ -641,9 +642,9 @@ fn interpret_vertex_fetch(t: u32) {
|
|||||||
let w0 = vs_instr_dword(t, 0u);
|
let w0 = vs_instr_dword(t, 0u);
|
||||||
let w1 = vs_instr_dword(t, 1u);
|
let w1 = vs_instr_dword(t, 1u);
|
||||||
let w2 = vs_instr_dword(t, 2u);
|
let w2 = vs_instr_dword(t, 2u);
|
||||||
let fetch_const = (w0 >> 5u) & 0x1Fu;
|
let fetch_const = (w0 >> 20u) & 0x1Fu;
|
||||||
let dst_reg = (w0 >> 10u) & 0x7Fu;
|
let dst_reg = (w0 >> 12u) & 0x3Fu;
|
||||||
let src_reg = (w0 >> 17u) & 0x7Fu;
|
let src_reg = (w0 >> 5u) & 0x3Fu;
|
||||||
let format = (w1 >> 16u) & 0x3Fu;
|
let format = (w1 >> 16u) & 0x3Fu;
|
||||||
let stride = w2 & 0xFFu;
|
let stride = w2 & 0xFFu;
|
||||||
|
|
||||||
@@ -773,20 +774,20 @@ fn interpret_texture_fetch(t: u32, is_vertex: bool) {
|
|||||||
} else {
|
} else {
|
||||||
w0 = ps_instr_dword(t, 0u);
|
w0 = ps_instr_dword(t, 0u);
|
||||||
}
|
}
|
||||||
let dst_reg = (w0 >> 10u) & 0x7Fu;
|
let dst_reg = (w0 >> 12u) & 0x3Fu;
|
||||||
let src_reg = (w0 >> 17u) & 0x7Fu;
|
let src_reg = (w0 >> 5u) & 0x3Fu;
|
||||||
let uv = registers[src_reg & 0x7Fu].xy;
|
let uv = registers[src_reg & 0x3Fu].xy;
|
||||||
let sample = textureSampleLevel(xenos_tex, xenos_samp, uv, 0.0);
|
let sample = textureSampleLevel(xenos_tex, xenos_samp, uv, 0.0);
|
||||||
registers[dst_reg & 0x7Fu] = sample;
|
registers[dst_reg & 0x3Fu] = sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk an Exec clause's instruction triples.
|
// Walk an Exec clause's instruction triples.
|
||||||
// sequence: 2-bit-per-triple bitmap. Bit 0 of a pair = serialize flag
|
// sequence: 2-bit-per-instruction bitmap. Bit 0 of a pair = fetch(1)/ALU(0);
|
||||||
// (we ignore in MVP); bit 1 = is-fetch.
|
// bit 1 = serialize (ignored). (Xenos `ucode.h:226`.)
|
||||||
fn exec_vs(address: u32, count: u32, sequence: u32) {
|
fn exec_vs(address: u32, count: u32, sequence: u32) {
|
||||||
for (var i: u32 = 0u; i < count; i = i + 1u) {
|
for (var i: u32 = 0u; i < count; i = i + 1u) {
|
||||||
let t = address + i;
|
let t = address + i;
|
||||||
let is_fetch = ((sequence >> (i * 2u + 1u)) & 1u) != 0u;
|
let is_fetch = ((sequence >> (i * 2u)) & 1u) != 0u;
|
||||||
if is_fetch {
|
if is_fetch {
|
||||||
let opcode = vs_instr_dword(t, 0u) & 0x1Fu;
|
let opcode = vs_instr_dword(t, 0u) & 0x1Fu;
|
||||||
// 0x00 = vertex fetch, 0x01 = texture fetch.
|
// 0x00 = vertex fetch, 0x01 = texture fetch.
|
||||||
@@ -803,7 +804,7 @@ fn exec_vs(address: u32, count: u32, sequence: u32) {
|
|||||||
fn exec_ps(address: u32, count: u32, sequence: u32) {
|
fn exec_ps(address: u32, count: u32, sequence: u32) {
|
||||||
for (var i: u32 = 0u; i < count; i = i + 1u) {
|
for (var i: u32 = 0u; i < count; i = i + 1u) {
|
||||||
let t = address + i;
|
let t = address + i;
|
||||||
let is_fetch = ((sequence >> (i * 2u + 1u)) & 1u) != 0u;
|
let is_fetch = ((sequence >> (i * 2u)) & 1u) != 0u;
|
||||||
if is_fetch {
|
if is_fetch {
|
||||||
interpret_texture_fetch(t, false);
|
interpret_texture_fetch(t, false);
|
||||||
} else {
|
} else {
|
||||||
@@ -962,6 +963,9 @@ fn walk_cf_vs() {
|
|||||||
// No call stack — mark and continue.
|
// No call stack — mark and continue.
|
||||||
reject_mask |= REJECT_CF_CALL;
|
reject_mask |= REJECT_CF_CALL;
|
||||||
}
|
}
|
||||||
|
case CF_KIND_NOP: {
|
||||||
|
// kNop padding / kMarkVsFetchDone hint — no-op, just advance.
|
||||||
|
}
|
||||||
default: { reject_mask |= REJECT_CF_JUMP; }
|
default: { reject_mask |= REJECT_CF_JUMP; }
|
||||||
}
|
}
|
||||||
if stop { break; }
|
if stop { break; }
|
||||||
|
|||||||
@@ -237,6 +237,10 @@ impl EmitCtx {
|
|||||||
current_alloc = *kind;
|
current_alloc = *kind;
|
||||||
}
|
}
|
||||||
ControlFlowInstruction::Exit => break,
|
ControlFlowInstruction::Exit => break,
|
||||||
|
// Non-executing CF clauses: padding (`kNop`) and the
|
||||||
|
// vertex-fetch-done hint (`kMarkVsFetchDone`). Skip them.
|
||||||
|
ControlFlowInstruction::Nop
|
||||||
|
| ControlFlowInstruction::MarkVsFetchDone => {}
|
||||||
ControlFlowInstruction::LoopStart { .. }
|
ControlFlowInstruction::LoopStart { .. }
|
||||||
| ControlFlowInstruction::LoopEnd { .. } => return Err(reject::CF_LOOP),
|
| ControlFlowInstruction::LoopEnd { .. } => return Err(reject::CF_LOOP),
|
||||||
ControlFlowInstruction::CondJmp { .. } => return Err(reject::CF_COND),
|
ControlFlowInstruction::CondJmp { .. } => return Err(reject::CF_COND),
|
||||||
@@ -284,7 +288,9 @@ impl EmitCtx {
|
|||||||
parsed.instructions[base + 1],
|
parsed.instructions[base + 1],
|
||||||
parsed.instructions[base + 2],
|
parsed.instructions[base + 2],
|
||||||
];
|
];
|
||||||
let is_fetch = ((sequence >> (i * 2 + 1)) & 1) != 0;
|
// sequence: 2 bits per instruction — bit[0]=fetch(1)/ALU(0),
|
||||||
|
// bit[1]=serialize (Xenos `ucode.h:226`).
|
||||||
|
let is_fetch = ((sequence >> (i * 2)) & 1) != 0;
|
||||||
if is_fetch {
|
if is_fetch {
|
||||||
match decode_fetch(words) {
|
match decode_fetch(words) {
|
||||||
FetchInstruction::Vertex(vf) => self.emit_vfetch(&vf)?,
|
FetchInstruction::Vertex(vf) => self.emit_vfetch(&vf)?,
|
||||||
|
|||||||
@@ -43,7 +43,15 @@ pub enum ControlFlowInstruction {
|
|||||||
Return,
|
Return,
|
||||||
/// `kAlloc` — pre-allocate export registers (position, interpolators, colors).
|
/// `kAlloc` — pre-allocate export registers (position, interpolators, colors).
|
||||||
Alloc { size: u32, kind: AllocKind },
|
Alloc { size: u32, kind: AllocKind },
|
||||||
/// Exit the shader (terminal).
|
/// `kNop` — fills space in the CF block; executes nothing, does not end
|
||||||
|
/// the shader. (Xenos opcode 0.)
|
||||||
|
Nop,
|
||||||
|
/// `kMarkVsFetchDone` — hint that no more vertex fetches will be performed.
|
||||||
|
/// (Xenos opcode 15.) Non-terminating.
|
||||||
|
MarkVsFetchDone,
|
||||||
|
/// Exit the shader (terminal). Synthesized — Xenos has no dedicated exit
|
||||||
|
/// opcode; the shader ends after an `Exec`/`CondExec` clause with the
|
||||||
|
/// END bit set (`is_end`). Retained for callers/tests that reference it.
|
||||||
Exit,
|
Exit,
|
||||||
/// Unknown / unhandled opcode.
|
/// Unknown / unhandled opcode.
|
||||||
Unknown { opcode: u8 },
|
Unknown { opcode: u8 },
|
||||||
@@ -93,37 +101,45 @@ fn decode_single(payload: u64) -> ControlFlowInstruction {
|
|||||||
let predicated = ((payload >> 28) & 1) != 0;
|
let predicated = ((payload >> 28) & 1) != 0;
|
||||||
let predicate_condition = ((payload >> 29) & 1) != 0;
|
let predicate_condition = ((payload >> 29) & 1) != 0;
|
||||||
|
|
||||||
|
// Xenos `ControlFlowOpcode` (canary `ucode.h:86-160`):
|
||||||
|
// 0 kNop, 1 kExec, 2 kExecEnd, 3 kCondExec, 4 kCondExecEnd,
|
||||||
|
// 5 kCondExecPred, 6 kCondExecPredEnd, 7 kLoopStart, 8 kLoopEnd,
|
||||||
|
// 9 kCondCall, 10 kReturn, 11 kCondJmp, 12 kAlloc,
|
||||||
|
// 13 kCondExecPredClean, 14 kCondExecPredCleanEnd, 15 kMarkVsFetchDone.
|
||||||
|
// All exec variants share the address(12)/count(3)/sequence(12) layout
|
||||||
|
// of `ControlFlowExecInstruction`; the `*End` variants terminate the
|
||||||
|
// shader. (Prior table was off-by-one — it mapped 0→Exec and 1→Exit,
|
||||||
|
// so a real `kExec` clause was misread as a terminal `Exit`, truncating
|
||||||
|
// the CF block and dropping every `tfetch` in it.)
|
||||||
|
let exec = |is_end: bool| ControlFlowInstruction::Exec {
|
||||||
|
address: (payload & 0xFFF) as u32,
|
||||||
|
count: ((payload >> 12) & 0x7) as u32,
|
||||||
|
sequence: ((payload >> 16) & 0xFFF) as u32,
|
||||||
|
is_end,
|
||||||
|
predicated,
|
||||||
|
predicate_condition,
|
||||||
|
};
|
||||||
match opcode {
|
match opcode {
|
||||||
0 => ControlFlowInstruction::Exec {
|
0 => ControlFlowInstruction::Nop,
|
||||||
address: (payload & 0xFFF) as u32,
|
1 => exec(false),
|
||||||
count: ((payload >> 12) & 0x7) as u32,
|
2 => exec(true),
|
||||||
sequence: ((payload >> 16) & 0xFFF) as u32,
|
3 => exec(false),
|
||||||
is_end: false,
|
4 => exec(true),
|
||||||
predicated,
|
5 => exec(false),
|
||||||
predicate_condition,
|
6 => exec(true),
|
||||||
},
|
7 => ControlFlowInstruction::LoopStart {
|
||||||
1 => ControlFlowInstruction::Exit,
|
|
||||||
2 => ControlFlowInstruction::Exec {
|
|
||||||
address: (payload & 0xFFF) as u32,
|
|
||||||
count: ((payload >> 12) & 0x7) as u32,
|
|
||||||
sequence: ((payload >> 16) & 0xFFF) as u32,
|
|
||||||
is_end: true,
|
|
||||||
predicated,
|
|
||||||
predicate_condition,
|
|
||||||
},
|
|
||||||
6 => ControlFlowInstruction::LoopStart {
|
|
||||||
address: (payload & 0x3FF) as u32,
|
address: (payload & 0x3FF) as u32,
|
||||||
loop_id: ((payload >> 16) & 0x1F) as u32,
|
loop_id: ((payload >> 16) & 0x1F) as u32,
|
||||||
},
|
},
|
||||||
7 => ControlFlowInstruction::LoopEnd {
|
8 => ControlFlowInstruction::LoopEnd {
|
||||||
address: (payload & 0x3FF) as u32,
|
address: (payload & 0x3FF) as u32,
|
||||||
loop_id: ((payload >> 16) & 0x1F) as u32,
|
loop_id: ((payload >> 16) & 0x1F) as u32,
|
||||||
},
|
},
|
||||||
8 => ControlFlowInstruction::CondCall {
|
9 => ControlFlowInstruction::CondCall {
|
||||||
target: (payload & 0x3FF) as u32,
|
target: (payload & 0x3FF) as u32,
|
||||||
},
|
},
|
||||||
9 => ControlFlowInstruction::Return,
|
10 => ControlFlowInstruction::Return,
|
||||||
10 => ControlFlowInstruction::CondJmp {
|
11 => ControlFlowInstruction::CondJmp {
|
||||||
target: (payload & 0x3FF) as u32,
|
target: (payload & 0x3FF) as u32,
|
||||||
predicated,
|
predicated,
|
||||||
predicate_condition,
|
predicate_condition,
|
||||||
@@ -132,6 +148,9 @@ fn decode_single(payload: u64) -> ControlFlowInstruction {
|
|||||||
size: (payload & 0x7) as u32,
|
size: (payload & 0x7) as u32,
|
||||||
kind: AllocKind::from_bits(((payload >> 4) & 0x7) as u32),
|
kind: AllocKind::from_bits(((payload >> 4) & 0x7) as u32),
|
||||||
},
|
},
|
||||||
|
13 => exec(false),
|
||||||
|
14 => exec(true),
|
||||||
|
15 => ControlFlowInstruction::MarkVsFetchDone,
|
||||||
other => ControlFlowInstruction::Unknown { opcode: other },
|
other => ControlFlowInstruction::Unknown { opcode: other },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,12 +160,49 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opcode_exit_decodes() {
|
fn opcode_nop_and_exec_decode() {
|
||||||
// opcode 1 (Exit) in bits 44..47 of A's 48-bit payload.
|
// Xenos opcode 0 = kNop (non-terminating padding).
|
||||||
|
let payload: u64 = 0u64 << 44;
|
||||||
|
let (hi, lo) = ((payload & 0xFFFF_FFFF) as u32, ((payload >> 32) & 0xFFFF) as u32);
|
||||||
|
assert_eq!(decode_cf_pair(hi, lo, 0).0, ControlFlowInstruction::Nop);
|
||||||
|
// Xenos opcode 1 = kExec (executes instructions; NOT a terminal exit).
|
||||||
let payload: u64 = 1u64 << 44;
|
let payload: u64 = 1u64 << 44;
|
||||||
let (hi, lo) = ((payload & 0xFFFF_FFFF) as u32, ((payload >> 32) & 0xFFFF) as u32);
|
let (hi, lo) = ((payload & 0xFFFF_FFFF) as u32, ((payload >> 32) & 0xFFFF) as u32);
|
||||||
let cf = decode_cf_pair(hi, lo, 0).0;
|
match decode_cf_pair(hi, lo, 0).0 {
|
||||||
assert_eq!(cf, ControlFlowInstruction::Exit);
|
ControlFlowInstruction::Exec { is_end, .. } => assert!(!is_end),
|
||||||
|
other => panic!("opcode 1 should be non-end Exec, got {other:?}"),
|
||||||
|
}
|
||||||
|
// Xenos opcode 15 = kMarkVsFetchDone (non-terminating hint).
|
||||||
|
let payload: u64 = 15u64 << 44;
|
||||||
|
let (hi, lo) = ((payload & 0xFFFF_FFFF) as u32, ((payload >> 32) & 0xFFFF) as u32);
|
||||||
|
assert_eq!(
|
||||||
|
decode_cf_pair(hi, lo, 0).0,
|
||||||
|
ControlFlowInstruction::MarkVsFetchDone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_logo_shader_has_tfetch_clauses() {
|
||||||
|
// The publisher-logo pixel shader E59B2B3DA4AA9008 (captured from the
|
||||||
|
// canary oracle, byte-identical to the microcode our guest IM_LOADs).
|
||||||
|
// Regression for iterate-3M: the old off-by-one opcode table decoded
|
||||||
|
// its leading `kExec` (opcode 1) as a terminal `Exit`, truncating the
|
||||||
|
// CF block so the `tfetch2D` never appeared → flat splash.
|
||||||
|
let ucode: [u32; 24] = [
|
||||||
|
0x00011002, 0x00001200, 0xC4000000, 0x00004003, 0x00002200, 0x00000000,
|
||||||
|
0x10082021, 0x1F1FF688, 0x00004000, 0xC8080001, 0x001B1B00, 0xC1020000,
|
||||||
|
0xC8070000, 0x00C0C000, 0xC1020000, 0xC8070001, 0x00C01B00, 0xC1000100,
|
||||||
|
0xC80F8000, 0x00000000, 0xC2010100, 0x00000000, 0x00000000, 0x00000000,
|
||||||
|
];
|
||||||
|
let p = crate::ucode::parse_shader(&ucode);
|
||||||
|
let exec_clauses = p
|
||||||
|
.cf
|
||||||
|
.iter()
|
||||||
|
.filter(|c| matches!(c, ControlFlowInstruction::Exec { .. }))
|
||||||
|
.count();
|
||||||
|
assert!(exec_clauses >= 1, "expected >=1 Exec clause, cf={:?}", p.cf);
|
||||||
|
let slots = crate::shader_metrics::tfetch_slots(&p);
|
||||||
|
assert!(!slots.is_empty(), "expected tfetch slots, got none; cf={:?}", p.cf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -54,23 +54,32 @@ pub mod op {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction {
|
pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction {
|
||||||
|
// Fetch dword0 bitfields (Xenos `ucode.h:740-749` vfetch / `844-845`
|
||||||
|
// tfetch): opcode_value:5, src_reg:6, src_reg_am:1, dst_reg:6,
|
||||||
|
// dst_reg_am:1, (fetch_valid_only|must_be_one):1, const_index:5 @ bit20,
|
||||||
|
// ... The prior decoder read `const_index` from bit 5 (which is actually
|
||||||
|
// `src_reg`), so every fetch reported the wrong fetch-constant slot — the
|
||||||
|
// logo `tfetch2D ..., tf0` was read as `tf1`, and slot 1's empty constant
|
||||||
|
// failed to decode → no texture. The texture-fetch `dimension` lives in
|
||||||
|
// dword2 bits 14..15, not dword1.
|
||||||
let w0 = words[0];
|
let w0 = words[0];
|
||||||
let w1 = words[1];
|
let w1 = words[1];
|
||||||
|
let w2 = words[2];
|
||||||
let opcode = (w0 & 0x1F) as u8;
|
let opcode = (w0 & 0x1F) as u8;
|
||||||
match opcode {
|
match opcode {
|
||||||
op::VERTEX_FETCH => FetchInstruction::Vertex(VertexFetch {
|
op::VERTEX_FETCH => FetchInstruction::Vertex(VertexFetch {
|
||||||
fetch_const: ((w0 >> 5) & 0x1F) as u8,
|
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
||||||
src_register: ((w0 >> 17) & 0x7F) as u8,
|
src_register: ((w0 >> 5) & 0x3F) as u8,
|
||||||
dest_register: ((w0 >> 10) & 0x7F) as u8,
|
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
||||||
dest_write_mask: ((w1 >> 23) & 0xF) as u8,
|
dest_write_mask: (w1 & 0xF) as u8,
|
||||||
raw: words,
|
raw: words,
|
||||||
}),
|
}),
|
||||||
op::TEXTURE_FETCH => FetchInstruction::Texture(TextureFetch {
|
op::TEXTURE_FETCH => FetchInstruction::Texture(TextureFetch {
|
||||||
fetch_const: ((w0 >> 5) & 0x1F) as u8,
|
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
||||||
src_register: ((w0 >> 17) & 0x7F) as u8,
|
src_register: ((w0 >> 5) & 0x3F) as u8,
|
||||||
dest_register: ((w0 >> 10) & 0x7F) as u8,
|
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
||||||
dest_write_mask: ((w1 >> 23) & 0xF) as u8,
|
dest_write_mask: (w1 & 0xF) as u8,
|
||||||
dimension: ((w1 >> 29) & 0x3) as u8,
|
dimension: ((w2 >> 14) & 0x3) as u8,
|
||||||
raw: words,
|
raw: words,
|
||||||
}),
|
}),
|
||||||
_ => FetchInstruction::Unknown { opcode, raw: words },
|
_ => FetchInstruction::Unknown { opcode, raw: words },
|
||||||
@@ -83,8 +92,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_vertex_fetch() {
|
fn decode_vertex_fetch() {
|
||||||
// opcode=0 (vertex), fetch_const=5, src=2, dest=7.
|
// opcode=0 (vertex). Xenos dword0: src_reg@bit5, dst_reg@bit12,
|
||||||
let w0 = 0u32 | (5 << 5) | (7 << 10) | (2 << 17);
|
// const_index@bit20. fetch_const=5, src=2, dest=7.
|
||||||
|
let w0 = 0u32 | (2 << 5) | (7 << 12) | (5 << 20);
|
||||||
let v = decode_fetch([w0, 0, 0]);
|
let v = decode_fetch([w0, 0, 0]);
|
||||||
match v {
|
match v {
|
||||||
FetchInstruction::Vertex(vf) => {
|
FetchInstruction::Vertex(vf) => {
|
||||||
@@ -98,11 +108,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_texture_fetch() {
|
fn decode_texture_fetch() {
|
||||||
let w0 = 1u32 | (3 << 5) | (4 << 10) | (1 << 17);
|
// opcode=1 (texture). const_index@bit20=3, src@bit5=1, dst@bit12=4.
|
||||||
let t = decode_fetch([w0, (2u32 << 29), 0]);
|
// dimension lives in dword2 bits 14..15.
|
||||||
|
let w0 = 1u32 | (1 << 5) | (4 << 12) | (3 << 20);
|
||||||
|
let w2 = 2u32 << 14;
|
||||||
|
let t = decode_fetch([w0, 0, w2]);
|
||||||
match t {
|
match t {
|
||||||
FetchInstruction::Texture(tf) => {
|
FetchInstruction::Texture(tf) => {
|
||||||
assert_eq!(tf.fetch_const, 3);
|
assert_eq!(tf.fetch_const, 3);
|
||||||
|
assert_eq!(tf.src_register, 1);
|
||||||
|
assert_eq!(tf.dest_register, 4);
|
||||||
assert_eq!(tf.dimension, 2);
|
assert_eq!(tf.dimension, 2);
|
||||||
}
|
}
|
||||||
other => panic!("expected Texture, got {other:?}"),
|
other => panic!("expected Texture, got {other:?}"),
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ pub mod cf_kind {
|
|||||||
pub const COND_JMP: u32 = 6;
|
pub const COND_JMP: u32 = 6;
|
||||||
pub const COND_CALL: u32 = 7;
|
pub const COND_CALL: u32 = 7;
|
||||||
pub const RETURN: u32 = 8;
|
pub const RETURN: u32 = 8;
|
||||||
|
/// Non-executing CF clause: `kNop` padding or `kMarkVsFetchDone` hint.
|
||||||
|
/// The WGSL CF walker treats this as a no-op (advance, do not reject).
|
||||||
|
pub const NOP: u32 = 9;
|
||||||
pub const UNKNOWN: u32 = 15;
|
pub const UNKNOWN: u32 = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +139,7 @@ fn encode_cf(c: ControlFlowInstruction) -> (u32, u32, u32) {
|
|||||||
}
|
}
|
||||||
CondCall { target } => (cf_kind::COND_CALL, target, 0),
|
CondCall { target } => (cf_kind::COND_CALL, target, 0),
|
||||||
Return => (cf_kind::RETURN, 0, 0),
|
Return => (cf_kind::RETURN, 0, 0),
|
||||||
|
Nop | MarkVsFetchDone => (cf_kind::NOP, 0, 0),
|
||||||
Unknown { opcode } => (cf_kind::UNKNOWN, opcode as u32, 0),
|
Unknown { opcode } => (cf_kind::UNKNOWN, opcode as u32, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,9 +168,11 @@ pub struct ParsedShader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a shader blob. `raw_dwords` is a host-endian slice of the entire
|
/// Decode a shader blob. `raw_dwords` is a host-endian slice of the entire
|
||||||
/// microcode buffer (control flow + instructions). Heuristic: CF dword count
|
/// microcode buffer (control flow + instructions). The CF block is implicitly
|
||||||
/// is encoded in the first word's low 12 bits of the last exec clause —
|
/// bounded: we walk clause-pair rows until one terminates the shader (an
|
||||||
/// canary iterates until it hits a clause of kind `Exit`. We do the same.
|
/// `Exec`/`CondExec` clause with the END bit set, per Xenos). Everything after
|
||||||
|
/// that row is the instruction block; exec/loop addresses are then rebased to
|
||||||
|
/// be relative to it.
|
||||||
pub fn parse_shader(raw_dwords: &[u32]) -> ParsedShader {
|
pub fn parse_shader(raw_dwords: &[u32]) -> ParsedShader {
|
||||||
let mut cf = Vec::new();
|
let mut cf = Vec::new();
|
||||||
// CF clauses are 48-bit (word1 lo 16 + word0 = 48 or so per canary's
|
// CF clauses are 48-bit (word1 lo 16 + word0 = 48 or so per canary's
|
||||||
@@ -175,22 +181,50 @@ pub fn parse_shader(raw_dwords: &[u32]) -> ParsedShader {
|
|||||||
while i + 2 < raw_dwords.len() {
|
while i + 2 < raw_dwords.len() {
|
||||||
let a = decode_cf_pair(raw_dwords[i], raw_dwords[i + 1], raw_dwords[i + 2]);
|
let a = decode_cf_pair(raw_dwords[i], raw_dwords[i + 1], raw_dwords[i + 2]);
|
||||||
let (first, second) = a;
|
let (first, second) = a;
|
||||||
let seen_exit = matches!(
|
// The CF block ends after the clause that terminates the shader: an
|
||||||
first,
|
// `Exec` with the END bit set (Xenos `kExecEnd`/`kCondExec*End`), a
|
||||||
ControlFlowInstruction::Exit | ControlFlowInstruction::Unknown { .. }
|
// synthetic `Exit`, or an `Unknown` opcode (decode ran off the CF
|
||||||
) || matches!(
|
// block into instruction data — stop defensively). `Nop` padding
|
||||||
second,
|
// does NOT terminate. (Previously this stopped on the first `Exit`,
|
||||||
ControlFlowInstruction::Exit | ControlFlowInstruction::Unknown { .. }
|
// but with the corrected opcode table opcode 1 is `kExec`, not exit,
|
||||||
);
|
// so real exec clauses kept the parse going as intended.)
|
||||||
|
let terminates = |cf: &ControlFlowInstruction| {
|
||||||
|
matches!(
|
||||||
|
cf,
|
||||||
|
ControlFlowInstruction::Exec { is_end: true, .. }
|
||||||
|
| ControlFlowInstruction::Exit
|
||||||
|
| ControlFlowInstruction::Unknown { .. }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let seen_end = terminates(&first) || terminates(&second);
|
||||||
cf.push(first);
|
cf.push(first);
|
||||||
cf.push(second);
|
cf.push(second);
|
||||||
i += 3;
|
i += 3;
|
||||||
if seen_exit {
|
if seen_end {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Everything after `i` dwords is the instruction block.
|
// Everything after `i` dwords is the instruction block.
|
||||||
let instructions = raw_dwords[i..].to_vec();
|
let instructions = raw_dwords[i..].to_vec();
|
||||||
|
// Xenos exec/loop `address` fields are absolute instruction-triple indices
|
||||||
|
// counted from shader dword 0, but `instructions` here begins *after* the
|
||||||
|
// CF block. Rebase those addresses to be relative to the instruction block
|
||||||
|
// (subtract the CF triple count) so `address * 3` indexes `instructions`
|
||||||
|
// directly. (Without this, every exec read 3 dwords too far per CF triple —
|
||||||
|
// the publisher-logo `tfetch` triple was skipped → flat splash.)
|
||||||
|
let cf_triples = (i / 3) as u32;
|
||||||
|
for clause in cf.iter_mut() {
|
||||||
|
match clause {
|
||||||
|
ControlFlowInstruction::Exec { address, .. } => {
|
||||||
|
*address = address.saturating_sub(cf_triples);
|
||||||
|
}
|
||||||
|
ControlFlowInstruction::LoopStart { address, .. }
|
||||||
|
| ControlFlowInstruction::LoopEnd { address, .. } => {
|
||||||
|
*address = address.saturating_sub(cf_triples);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
ParsedShader { cf, instructions }
|
ParsedShader { cf, instructions }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,15 +269,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn trivial_exit_clause_stops_parsing() {
|
fn exec_end_clause_stops_parsing() {
|
||||||
// Two clauses: [NOP (kind=0), EXIT (kind=1)] encoded per canary.
|
// Row: clause B = kExecEnd (opcode 2) terminates the CF block.
|
||||||
// Exit clause is opcode 1 in the top 4 bits of the upper 16 bits.
|
// 48-bit payload of B occupies hi16(word1) + word2; opcode lives in
|
||||||
let w0 = 0u32; // clause A body
|
// bits 44..47 of that payload. Put opcode 2 there: payload bit 44 set
|
||||||
let w1 = (1u32 << 12) << 16; // upper 16 bits = 0x1000 → opcode=1 (EXIT) for clause A
|
// for the `2` → (2 << 44). In B's framing, bits 16..47 come from
|
||||||
let w2 = 0u32;
|
// word2, so word2 bit (44-16)=28 region holds the opcode nibble.
|
||||||
let p = parse_shader(&[w0, w1, w2, 0xDEAD_BEEF]);
|
let b_payload: u64 = 2u64 << 44; // kExecEnd
|
||||||
|
// B = lo16 from hi16(word1), hi from word2. Reconstruct word1/word2.
|
||||||
|
let word1 = ((b_payload & 0xFFFF) as u32) << 16; // B's low 16 bits → hi16(word1)
|
||||||
|
let word2 = ((b_payload >> 16) & 0xFFFF_FFFF) as u32;
|
||||||
|
let p = parse_shader(&[0, word1, word2, 0xDEAD_BEEF]);
|
||||||
assert!(!p.cf.is_empty());
|
assert!(!p.cf.is_empty());
|
||||||
// Exit detected → remaining dword is instruction data.
|
// ExecEnd detected in the first row → remaining dword is instruction data.
|
||||||
assert_eq!(p.instructions, vec![0xDEAD_BEEF]);
|
assert_eq!(p.instructions, vec![0xDEAD_BEEF]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1652,6 +1652,79 @@ fn nt_set_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XFileRenameInformation (10): move the backing file to a new path.
|
||||||
|
// Sylpheed's asset-cache decompresses each packed resource to a staging
|
||||||
|
// `cache:\<hash><tail>.tmp` then renames it into its final nested path
|
||||||
|
// `cache:\<hash>\<dir>\<file>`. Without an actual host-FS rename the
|
||||||
|
// nested target stays empty, the later read-back of the decompressed
|
||||||
|
// asset (e.g. the title logo texture `\69d8e45c\e\534ffea`) misses, and
|
||||||
|
// the logo never loads. Mirror canary `xboxkrnl_io_info.cc:226`
|
||||||
|
// (`X_FILE_RENAME_INFORMATION{ replace_existing@0, root_dir_handle@4,
|
||||||
|
// ansi_string@8 }` → `file->Rename(TranslateAnsiPath(ansi_string))`).
|
||||||
|
if info_class == 10 {
|
||||||
|
// Read the target path from the embedded ANSI_STRING at info_ptr+8.
|
||||||
|
let target_raw = match crate::path::read_ansi_string(mem, info_ptr + 8) {
|
||||||
|
Some(s) if !s.is_empty() => s,
|
||||||
|
_ => {
|
||||||
|
const STATUS_OBJECT_NAME_INVALID: u64 = 0xC000_0033;
|
||||||
|
ctx.gpr[3] = STATUS_OBJECT_NAME_INVALID;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Resolve the destination against the host cache backing dir. We only
|
||||||
|
// support renames within the writable `cache:` mount (the only place
|
||||||
|
// a guest can create files); disc/synth entries are read-only.
|
||||||
|
let new_host = state.resolve_cache_path(&target_raw);
|
||||||
|
// Current backing host path of the handle.
|
||||||
|
let old_host = match state.objects.get(&handle) {
|
||||||
|
Some(KernelObject::File { host_path: Some(hp), .. }) => Some(hp.clone()),
|
||||||
|
Some(KernelObject::File { .. }) => None,
|
||||||
|
_ => {
|
||||||
|
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let status: u64 = match (old_host, new_host) {
|
||||||
|
(Some(old), Some(new)) => {
|
||||||
|
if let Some(parent) = new.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
match std::fs::rename(&old, &new) {
|
||||||
|
Ok(()) => {
|
||||||
|
// Update the handle so subsequent I/O targets the new
|
||||||
|
// host path + guest path.
|
||||||
|
if let Some(KernelObject::File { path, host_path, .. }) =
|
||||||
|
state.objects.get_mut(&handle)
|
||||||
|
{
|
||||||
|
*path = crate::path::normalize_path(&target_raw);
|
||||||
|
*host_path = Some(new.clone());
|
||||||
|
}
|
||||||
|
tracing::info!(
|
||||||
|
"NtSetInformationFile rename cache {:?} -> {:?} ({:?})",
|
||||||
|
old, new, target_raw
|
||||||
|
);
|
||||||
|
STATUS_SUCCESS
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"NtSetInformationFile rename {:?} -> {:?} failed: {}",
|
||||||
|
old, new, e
|
||||||
|
);
|
||||||
|
STATUS_UNSUCCESSFUL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-cache (read-only VFS) source/target: acknowledge without a
|
||||||
|
// host move, matching the prior permissive behaviour.
|
||||||
|
_ => STATUS_SUCCESS,
|
||||||
|
};
|
||||||
|
if iosb_ptr != 0 {
|
||||||
|
write_io_status_block(mem, iosb_ptr, status as u32, info_length);
|
||||||
|
}
|
||||||
|
ctx.gpr[3] = status;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle lookup.
|
// Handle lookup.
|
||||||
let Some(KernelObject::File { size, position, host_path, .. }) = state.objects.get_mut(&handle) else {
|
let Some(KernelObject::File { size, position, host_path, .. }) = state.objects.get_mut(&handle) else {
|
||||||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||||||
@@ -5581,6 +5654,67 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `NtSetInformationFile` class 10 (`XFileRenameInformation`) must move
|
||||||
|
/// the backing host file to the new `cache:` path and update the handle.
|
||||||
|
/// Mirrors Sylpheed's asset-cache `.tmp` → `\<hash>\<dir>\<file>` move;
|
||||||
|
/// without it the nested target stays empty and the decompressed asset
|
||||||
|
/// (logo texture) never reads back. Faithful to canary `file->Rename`.
|
||||||
|
#[test]
|
||||||
|
fn nt_set_information_file_rename_moves_cache_file() {
|
||||||
|
let (mut ctx, mut mem, mut state) = fresh();
|
||||||
|
// Real temp cache root + a staging `.tmp` file with known bytes.
|
||||||
|
let root = std::env::temp_dir().join(format!("xenia-rs-rename-test-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
|
let old_host = root.join("69d8e45ce534ffea.tmp");
|
||||||
|
std::fs::write(&old_host, b"LOGOTEX!").unwrap();
|
||||||
|
state.cache_root = Some(root.clone());
|
||||||
|
// Open handle whose backing host_path is the staging file.
|
||||||
|
let handle = state.alloc_handle_for(KernelObject::File {
|
||||||
|
path: "69d8e45ce534ffea.tmp".to_string(),
|
||||||
|
size: 8,
|
||||||
|
position: 0,
|
||||||
|
data: Arc::new(Vec::new()),
|
||||||
|
dir_enum_pos: None,
|
||||||
|
host_path: Some(old_host.clone()),
|
||||||
|
});
|
||||||
|
// X_FILE_RENAME_INFORMATION { replace@0, root_dir@4, ANSI_STRING@8 }.
|
||||||
|
// ANSI_STRING { len u16, max u16, buf u32 } at info_ptr+8; buffer holds
|
||||||
|
// the target path "cache:\69d8e45c\e\534ffea".
|
||||||
|
let info_ptr = SCRATCH_BASE + 0x100;
|
||||||
|
let str_buf = SCRATCH_BASE + 0x200;
|
||||||
|
let target = b"cache:\\69d8e45c\\e\\534ffea";
|
||||||
|
for (i, b) in target.iter().enumerate() {
|
||||||
|
mem.write_u8(str_buf + i as u32, *b);
|
||||||
|
}
|
||||||
|
mem.write_u32(info_ptr, 0); // replace_existing
|
||||||
|
mem.write_u32(info_ptr + 4, 0); // root_dir_handle
|
||||||
|
mem.write_u16(info_ptr + 8, target.len() as u16); // ANSI_STRING.Length
|
||||||
|
mem.write_u16(info_ptr + 10, target.len() as u16); // MaximumLength
|
||||||
|
mem.write_u32(info_ptr + 12, str_buf); // Buffer
|
||||||
|
let iosb_ptr = SCRATCH_BASE + 0x140;
|
||||||
|
ctx.gpr[3] = handle as u64;
|
||||||
|
ctx.gpr[4] = iosb_ptr as u64;
|
||||||
|
ctx.gpr[5] = info_ptr as u64;
|
||||||
|
ctx.gpr[6] = 16;
|
||||||
|
ctx.gpr[7] = 10; // XFileRenameInformation
|
||||||
|
nt_set_information_file(&mut ctx, &mut mem, &mut state);
|
||||||
|
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||||||
|
// Staging file gone; nested target exists with the same bytes.
|
||||||
|
let new_host = root.join("69d8e45c").join("e").join("534ffea");
|
||||||
|
assert!(!old_host.exists(), "staging .tmp should be moved away");
|
||||||
|
assert_eq!(std::fs::read(&new_host).unwrap(), b"LOGOTEX!");
|
||||||
|
// Handle now points at the new host + guest path.
|
||||||
|
match state.objects.get(&handle) {
|
||||||
|
Some(KernelObject::File { host_path: Some(hp), path, .. }) => {
|
||||||
|
assert_eq!(hp, &new_host);
|
||||||
|
assert_eq!(path, "cache:/69d8e45c/e/534ffea");
|
||||||
|
}
|
||||||
|
_ => panic!("file handle lost or host_path missing"),
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
/// Read-only VFS — truncating to a different size must fail with
|
/// Read-only VFS — truncating to a different size must fail with
|
||||||
/// `STATUS_UNSUCCESSFUL`, matching Canary's error path when
|
/// `STATUS_UNSUCCESSFUL`, matching Canary's error path when
|
||||||
/// `file->SetLength(...)` can't honour the request.
|
/// `file->SetLength(...)` can't honour the request.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use xenia_memory::{GuestMemory, MemoryAccess};
|
|||||||
/// u16 Length
|
/// u16 Length
|
||||||
/// u16 MaximumLength
|
/// u16 MaximumLength
|
||||||
/// u32 Buffer (guest pointer)
|
/// u32 Buffer (guest pointer)
|
||||||
fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
|
pub fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user