Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tool to test DSP instructions #70

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 302 additions & 0 deletions python-scripts/dsp_instruction_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
#!/usr/bin/env python3

# Run this from a clean Xbox environment (= not while a game is running)
# NXDK-RDT is a good environment

from xboxpy import *

import json
import sys
import time

reserved_words = 0x20

def get_word(s):
#FIXME: Assert type is string or int
s = str(s)
s = s.strip()
s = s.replace('$', '0x')
s = s.lower()
if s[0:2] == "0x":
return int(s[2:], 16)
else:
return int(s, 10)

def dsp_stop():
# Reset GP and GP DSP
apu.write_u32(NV_PAPU_GPRST, 0)
time.sleep(0.1) # FIXME: Not sure if DSP reset is synchronous, so we wait for now

# Enable GP first, otherwise memory writes won't be handled
apu.write_u32(NV_PAPU_GPRST, NV_PAPU_GPRST_GPRST)


def dsp_allocate_scratch_space(page_count):

# Allocate some scratch space (at least 2 pages!)
#FIXME: Why did I put: "(at least 2 pages!)" ?
assert(page_count >= 2)

# Apparently NV_PAPU_GPSADDR has to be aligned to 0x4000 bytes?!
#FIXME: Free memory after running
page_head = ke.MmAllocateContiguousMemoryEx(4096, 0x00000000, 0xFFFFFFFF, 0x4000, ke.PAGE_READWRITE)
page_head_p = ke.MmGetPhysicalAddress(page_head)
apu.write_u32(NV_PAPU_GPSADDR, page_head_p)
page_base = ke.MmAllocateContiguousMemory(4096 * page_count)
for i in range(0, page_count):
write_u32(page_head + i * 8 + 0, ke.MmGetPhysicalAddress(page_base + 0x1000 * i))
write_u32(page_head + i * 8 + 4, 0) # Control

# I'm not sure if this is off-by-one (maybe `page_count - 1`)
apu.write_u32(NV_PAPU_GPSMAXSGE, page_count)

return page_base

def dsp_start():

# Set frame duration (?!)
if True:
apu.write_u32(NV_PAPU_SECTL, 3 << 3)

# Mark GP as ready?
NV_PAPU_GPIDRDY = 0x3FF10
NV_PAPU_GPIDRDY_GPSETIDLE = (1 << 0)
apu.write_u32(NV_PAPU_GPIDRDY, NV_PAPU_GPIDRDY_GPSETIDLE)

# Clear interrupts
NV_PAPU_GPISTS = 0x3FF14
apu.write_u32(NV_PAPU_GPISTS, 0xFF)

# Now run GP DSP by removing reset bit
apu.write_u32(NV_PAPU_GPRST, NV_PAPU_GPRST_GPRST | NV_PAPU_GPRST_GPDSPRST)
time.sleep(0.1)

def dsp_homebrew(tests):
#FIXME: Pass dsp object which provides all the device details and registers instead

# Stop the running DSP
print("Stopping DSP")
dsp_stop()

# Allocate new scratch space
print("Allocating DSP scratch space")
page_base = dsp_allocate_scratch_space(20)

# Start with empty code
print("Generating DSP code")
code = ""

# We reserve some program words (jump targets, as magic for p-word injection)
code += "nop\n" * reserved_words

# Setup memory registers
#FIXME: Do this before every writeback / address outputs directly
code += "\n; Memory register setup\n\n"
code += "movec #$FFFF, M7\n"
code += "move #$100, R7\n"
code += "move #1, N7\n"
code += "nop\n"

# Generate each test
r7 = 0x100 #FIXME: X:0 seems to be used during DSP reset?!
n7 = 1
for t in tests:
i = t[0]
p = t[1]
o = t[2]

code += "\n; Test\n\n"

#FIXME: Allow only 24 bit registers
# - Replace 'a' by ('a2', 'a1', 'a0')
# - Replace 'b' by ('b2', 'b1', 'b0')

control_regs = [
"vba", "sr", "omr", "sp", "ssh", "ssl", "la", "lc",
"m0", "m1", "m2", "m3", "m4", "m5", "m6", "m7",
]

# Add any register which would cause trouble
forbidden_regs = [
"ssh", # Crash in XQEMU (possibly bug in a56?)
"vba", # Caused error in a56
"m7", "r7", "n7" # Used for input/output addressing
]

# Input
for k, v in i.items():

if k.find(':') != -1:
print("Memory %s unsupported in setter" % k)
space, colon, address = k.partition(':')
address = get_word(address)

for w in v:
print("Would have set %s:$%X to 0x%06X" % (space, address, w))
address += 1

elif k.lower() in forbidden_regs:
print("Register %s forbidden in setter" % k)
else:
if k.lower() in control_regs:
code += "movec x:(R7)+N7, %s\n" % (k)
else:
code += "move x:(R7)+N7, %s\n" % (k)
apu.write_u32(NV_PAPU_GPXMEM + r7*4, v)
r7 += n7
code += "nop\n" # Avoid hazards

# Processing, while avoiding the mix any of the surrounding instructions
code += "nop\n"
code += p[0] + "\n"
code += "nop\n"

# Setup memory registers for output
code += "movec #$FFFF, M7\n"
code += "move #$100, R7\n"
code += "move #1, N7\n"
code += "nop\n"

# Output
for k in o:

if k.lower() in forbidden_regs:
print("Register %s forbidden in getter" % k)
code += "move a0, y:(R7)+N7\n" # Used to advance R7
elif k.lower() in control_regs:
code += "movec %s, y:(R7)+N7\n" % (k)
else:
code += "move %s, y:(R7)+N7\n" % (k)

# Mark results as ready and stick in a loop
code += "\n; Result marker\n\n"
code += "move #$1337, a\n"
code += "move a, y:$0\n"
code += "finished_loop\n"
code += " jmp finished_loop"

# Assemble code and convert the 24 bit words to 32 bit words
print("Starting assembler")
data = dsp.assemble(code)
data = dsp.from24(data)

# Write code to PMEM, and also inject p-words
print("Writing program to DSP")
for i in range(0, len(data) // 4):
word = int.from_bytes(data[i*4:i*4+4], byteorder='little', signed=False) & 0xFFFFFF

# Check for the magic jmp, to inject p-words
if word & 0xFFF000 == 0x0C0000:
jmp_label = word & 0xFFF
if jmp_label < reserved_words:
word = p[1][jmp_label]

apu.write_u32(NV_PAPU_GPPMEM + i*4, word)
# According to XQEMU, 0x800 * 4 bytes will be loaded from scratch to PMEM at startup.
# So just to be sure, let's also write this to the scratch..
write_u32(page_base + i*4, word)

# Set Y:0, so we can see when the program modifies it later
apu.write_u32(NV_PAPU_GPYMEM + 0*4, 0x000000)
assert(apu.read_u32(NV_PAPU_GPYMEM + 0*4) == 0x000000)

# Run the test
print("Running test")
dsp_start()

# Wait until results are ready
print("Waiting for test results")
while(apu.read_u32(NV_PAPU_GPYMEM + 0*4) != 0x1337):
time.sleep(0.5)

# Read all results from memory
print("Reading test results")
all_results = []
r7 = 0x100
n7 = 1
for t in tests:
i = t[0]
p = t[1]
o = t[2]

#FIXME: Verify that inputs were not touched

# Loop over all outputs
results = {}
for k in o:
results[k] = apu.read_u32(NV_PAPU_GPYMEM + r7*4)
r7 += n7

# Add result for this test
all_results += [results]

# Remove forbidden outputs
for k in forbidden_regs:
if k in results:
del results[k]

# Return the output
return all_results




# Process all tests
for path in sys.argv[1:]:

print("")
print("Processing '%s'" % path)
print("")

# Load test from file
#FIXME: Error checking
with open(path, 'rb') as f:
source = f.read()
test = json.loads(source)

# Turn all prefixed strings into words
new_input = {}
for k, v in test['input'].items():
if k.find(':') != -1:
new_input[k] = [get_word(word) for word in v]
else:
new_input[k] = get_word(v)
new_input

# Turn line-array into string
new_code = ""
inject = []
for l in test['code']:

# Check for raw p-words to inject
if l[0] == ":":
v = get_word(l[1:])
print("Unsupported program-word: 0x%06X" % v)
#FIXME: We need some syntax for parallel words?
new_code += "jmp <$%X\n" % len(inject)
inject += [v]
assert(len(inject) <= reserved_words)

else:
new_code += l + "\n"

#FIXME: This is a legacy format; this weird conversion shouldn't be necessary
# Assemble a test
tests = [(new_input, (new_code, inject), test['output'])]

# Run the tests
results = dsp_homebrew(tests)

# Print results
print("Finished:")
output_json = {}
for r in results:
print("")
for k, v in r.items():
print("%s: 0x%X" % (k, v))
output_json[k] = '0x%X' % (v)

#FIXME: Write to log
output_source = json.dumps(output_json, indent=2)
with open(path + "-out.json", 'wb') as f:
f.write(output_source.encode("utf-8"))
15 changes: 15 additions & 0 deletions python-scripts/dsp_instruction_tests/code-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"input": {
"a2": "0x00",
"a1": "0x000000",
"a0": "0x000000"
},
"code": [
"move #$12, a2",
"move #$345678, a1",
"move #$9ABCDE, a0"
],
"output": [
"a2","a1","a0"
]
}
15 changes: 15 additions & 0 deletions python-scripts/dsp_instruction_tests/inc_a-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"input": {
"a2": "0x00",
"a1": "0x000000",
"a0": "0x000000"
},
"code": [
":0x000008", "; inc a"
],
"output": [
"a2",
"a1",
"a0"
]
}
13 changes: 13 additions & 0 deletions python-scripts/dsp_instruction_tests/memory-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"input": {
"x:$10": ["0xFF0012","0x123456"],
"x:$20": ["0x789ABC"]
},
"code": [
"move x:$10, x0",
"move x0, x:$20"
],
"output": [
"x0"
]
}
16 changes: 16 additions & 0 deletions python-scripts/dsp_instruction_tests/registers-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"input": {
"a2": "0x00", "a1": "0x000000", "a0": "0x000000"
},
"code": [],
"output": [
"vba", "sr", "omr", "sp", "ssh", "ssl", "la", "lc",
"a2","a1","a0",
"b2","b1","b0",
"x0", "x1",
"y0", "y1",
"r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7",
"m0", "m1", "m2", "m3", "m4", "m5", "m6", "m7",
"n0", "n1", "n2", "n3", "n4", "n5", "n6", "n7"
]
}