mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-07 17:29:58 -10:00
The sound processor's recompiler on ARM64 was the old one (arm7.cpp with arm64.cpp), which makes its blocks its own way: where a block ends is where the time slice can end and an interrupt be taken, so an ARM64 machine and an x86-64 one parted in the first frame (tools/determinism, Jet Grind Radio, in arm_Reg) unless both ran the interpreter. ARM64 now uses the recompiler x86-64 uses - arm7_rec.cpp's decoder, blocks, cycle counts and register allocator - with a backend of its own, arm7_rec_arm64.cpp, ported from upstream flycast's (flyinghead, core/hw/arm7/arm7_rec_arm64.cpp) and made to do what this fork's x86-64 backend does for every instruction, which upstream's did not always: - a block's cycles come off at its end, and the time slice is over at none left or less, as on x86-64 (upstream: at the top, and at less than none); - the ARM's flags stay in its register file and are taken into the host's NZCV only for the instruction that needs them; a logical instruction that sets them leaves V, and its carry is the shift's or the constant's, as x86-64 has it; RSB and RSC take their carry from the subtraction they are (upstream added the negation, wrong for 0); - a shift by a register takes the register's low byte; - consecutive instructions with the same condition share one test, and registers are written back before conditional code, as on x86-64; - loads and stores to sound memory are done in line, the rest through the call; a block load reads a whole word; the program counter loses its low two bits wherever it is written; - each block jumps on to the next itself, through the address's slot when it is fixed. The code is kept where the old one was: in the core's .text on Linux and Android (calls to the core are one branch), and on macOS on Apple silicon in MAP_JIT memory mapped once, written between vmem_jit_write_begin() and end(), with calls made through a register where the core is out of a branch's reach; without JIT memory the interpreter runs. arm64.cpp and the old recompiler's ARM64 parts in arm7.cpp are removed; 32-bit ARM keeps the old one. One change to the shared allocator, for both backends: writing the status register (MSR) can switch the mode and with it the banked registers r8-r14, which MSR_do() swaps in the register file. Registers are now written back before an MSR and read again after it, as around the interpreter. Which values were still in host registers depended on how many the host has (6 on x86-64 Linux, 8 on Windows and ARM64): with tools/arm7's programs made to change mode (ARM7_TEST_MODES, new), the x86-64 recompiler disagreed with the interpreter on 112 of some 8700 before this, and on none after. tools/arm7/run.sh runs on ARM64 too (natively, or a cross build under qemu: ARCH, MAKEARGS, RUN), and can keep what the recompiled programs left (ARM7_TEST_OUT), for comparing machines. The core option text no longer says the recompilers differ. Tested here: - tools/arm7, 10000 programs (8844 compared), and 10000 with mode changes (8694 compared): the recompiler, the interpreter and changing between the two agree, on x86-64 and on AArch64 under qemu; what the recompiled programs leave is the same on the two machines, line for line, in both sets - tools/threads/headless.sh passes: Linux x86-64, and AArch64 under qemu - tools/determinism, default options (the ARM7 recompiler on on both), Jet Grind Radio: x86-64 against AArch64 under qemu the same at every frame, 1000 frames from each of two states (in game, character select); on x86-64 the reload, run-ahead and rollback runs are plain's - the three ARM7 files compile for arm64-apple-macos (clang 18, MacOSX 13.3 SDK headers) with HOST_APPLE_JIT on; not linked, not run on a Mac No save-state change. Nothing compiled, allocated or waited for that was not before: blocks are compiled as they were, when first reached.
293 lines
11 KiB
C++
293 lines
11 KiB
C++
/* Runs the sound chip's ARM7 on made-up programs and prints what each
|
|
* leaves behind. tools/arm7/run.sh builds this twice, once with the
|
|
* recompiler (core/hw/arm7/arm7_rec.cpp, arm7_rec_x64.cpp) and once with
|
|
* the interpreter (arm7.cpp, arm-new.h), runs both and compares what they
|
|
* print: registers, flags and memory have to be the same from both after
|
|
* every program.
|
|
*
|
|
* A program is 8 to 40 random instructions and then a branch to itself:
|
|
* arithmetic and logic with every kind of second operand, shift and
|
|
* condition, with and without setting flags; multiplies; loads and stores
|
|
* of words and bytes with every addressing mode; block loads and stores;
|
|
* reads and writes of the flags; and branches, with and without link,
|
|
* forward only, so that every program gets to its end. It starts from
|
|
* random registers that point into the middle of memory.
|
|
*
|
|
* Left out: anything that writes r15 other than a branch, changes of mode
|
|
* (but with $ARM7_TEST_MODES: see below), software interrupts and undefined instructions, and the block transfers
|
|
* and writebacks the ARM manual calls unpredictable. A program that stores
|
|
* over its own instructions is not compared: the recompiler does not look
|
|
* for that, and the sound drivers do not do it.
|
|
*
|
|
* arm7_test [programs [first]] runs that many, from the first given.
|
|
* $ARM7_TEST_INTERPRET has the build with the recompiler run them through
|
|
* the interpreter it also has, and $ARM7_TEST_SWITCH has it change between
|
|
* the two over its first samples; see run.sh.
|
|
* $ARM7_TEST_MODES has one instruction in ten change the processor's mode,
|
|
* to one of those with banked registers or system mode, with interrupts
|
|
* off: other programs than without it.
|
|
* $ARM7_TEST_CASE runs one; $ARM7_TEST_PRINT also prints its instructions,
|
|
* and $ARM7_TEST_PROGRAM names a file of hex words to run in their place. */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <setjmp.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
#include "types.h"
|
|
#include "hw/arm7/arm7.h"
|
|
#include "hw/aica/aica_if.h"
|
|
#include "log/Log.h"
|
|
|
|
settings_t settings;
|
|
unsigned ARAM_SIZE = 2 * 1024 * 1024;
|
|
unsigned ARAM_MASK = 2 * 1024 * 1024 - 1;
|
|
VArray2 aica_ram;
|
|
static u8 ram[2 * 1024 * 1024] __attribute__((aligned(4096)));
|
|
|
|
/* A check in the ARM7 code failed. That ends the program being run, not
|
|
* the test: a program that has stored over its own instructions goes on to
|
|
* run whatever it stored, and that can be anything. */
|
|
static jmp_buf stopped;
|
|
void os_DebugBreak() { longjmp(stopped, 1); }
|
|
void GenericLog(LogTypes::LOG_LEVELS, LogTypes::LOG_TYPE, const char*, int, const char*, ...) {}
|
|
u32 libAICA_ReadReg(u32, u32) { return 0; }
|
|
void libAICA_WriteReg(u32, u32, u32) {}
|
|
void libAICA_TimeStep() {}
|
|
|
|
bool vmem_platform_prepare_jit_block(void *code_area, unsigned size, void **code_area_rwx)
|
|
{
|
|
const uintptr_t page = (uintptr_t)sysconf(_SC_PAGESIZE);
|
|
const uintptr_t from = (uintptr_t)code_area & ~(page - 1);
|
|
const uintptr_t to = ((uintptr_t)code_area + size + page - 1) & ~(page - 1);
|
|
|
|
if (mprotect((void*)from, to - from, PROT_READ | PROT_WRITE | PROT_EXEC))
|
|
return false;
|
|
*code_area_rwx = code_area;
|
|
return true;
|
|
}
|
|
|
|
void vmem_platform_flush_cache(void *icache_start, void *icache_end, void *dcache_start, void *dcache_end)
|
|
{
|
|
__builtin___clear_cache((char *)icache_start, (char *)icache_end);
|
|
}
|
|
|
|
static bool modes;
|
|
|
|
static u64 seed;
|
|
static u32 rnd(void)
|
|
{
|
|
seed = seed * 6364136223846793005ull + 1442695040888963407ull;
|
|
return (u32)(seed >> 33);
|
|
}
|
|
static u32 pick(u32 n) { return rnd() % n; }
|
|
|
|
#define PROGRAM 0x1000u /* where the program is */
|
|
|
|
static u32 cond(void)
|
|
{
|
|
/* always, three times in four; else any condition but "never" */
|
|
return (pick(4) ? 14u : pick(14)) << 28;
|
|
}
|
|
|
|
/* a register for a value: r0-r12, now and then r15, which reads as the
|
|
* address of the instruction plus 8 */
|
|
static u32 src_reg(void) { return pick(24) ? pick(13) : 15; }
|
|
static u32 dst_reg(void) { return pick(13); }
|
|
|
|
static u32 operand2(void)
|
|
{
|
|
switch (pick(3))
|
|
{
|
|
case 0: return (1u << 25) | (pick(16) << 8) | pick(256); /* rotated immediate */
|
|
case 1: return (pick(32) << 7) | (pick(4) << 5) | src_reg(); /* shift by a count */
|
|
default: return (pick(13) << 8) | (pick(4) << 5) | (1u << 4) | pick(13); /* shift by a register */
|
|
}
|
|
}
|
|
|
|
static u32 instruction(unsigned at, unsigned count)
|
|
{
|
|
if (modes && pick(10) == 0)
|
|
{
|
|
/* MSR CPSR_c, #imm: interrupts off, and FIQ, IRQ, supervisor, abort,
|
|
* undefined or system mode */
|
|
static const u32 mode[] = { 0x11, 0x12, 0x13, 0x17, 0x1b, 0x1f };
|
|
return cond() | 0x0321F0C0 | mode[pick(6)];
|
|
}
|
|
switch (pick(16))
|
|
{
|
|
default: /* arithmetic and logic */
|
|
{
|
|
const u32 op = pick(16);
|
|
const bool test = op >= 8 && op <= 11; /* TST TEQ CMP CMN: flags only */
|
|
return cond() | (op << 21) | ((test || pick(2)) ? 1u << 20 : 0)
|
|
| (src_reg() << 16) | ((test ? 0 : dst_reg()) << 12) | operand2();
|
|
}
|
|
case 9: /* multiply, multiply and add */
|
|
{
|
|
u32 rd = dst_reg(), rm = pick(13);
|
|
if (rm == rd)
|
|
rm = (rm + 1) % 13;
|
|
return cond() | (pick(2) << 21) | (pick(2) << 20) | (rd << 16) | (pick(13) << 12)
|
|
| (pick(13) << 8) | 0x90 | rm;
|
|
}
|
|
case 10: case 11: case 12: /* load or store, word or byte */
|
|
{
|
|
const u32 rn = pick(13);
|
|
u32 rd = pick(13);
|
|
const u32 pre = pick(2), wb = pick(2);
|
|
u32 offset;
|
|
if (rd == rn)
|
|
rd = (rd + 1) % 13;
|
|
if (pick(2))
|
|
offset = pick(4096);
|
|
else
|
|
{
|
|
u32 rm = pick(13);
|
|
if (rm == rn)
|
|
rm = (rm + 1) % 13;
|
|
offset = (1u << 25) | (pick(32) << 7) | (pick(4) << 5) | rm;
|
|
}
|
|
return cond() | (1u << 26) | (pre << 24) | (pick(2) << 23) | (pick(2) << 22)
|
|
| ((pre ? wb : 0) << 21) | (pick(2) << 20) | (rn << 16) | (rd << 12) | offset;
|
|
}
|
|
case 13: /* block load or store */
|
|
{
|
|
const u32 rn = pick(13), load = pick(2);
|
|
u32 list = rnd() & 0x1FFF;
|
|
list &= ~(1u << rn); /* the base is not in the list */
|
|
if (!list)
|
|
list = 1u << ((rn + 1) % 13);
|
|
return cond() | (4u << 25) | (pick(2) << 24) | (pick(2) << 23) | (pick(2) << 21)
|
|
| (load << 20) | (rn << 16) | list;
|
|
}
|
|
case 14: /* the flags, read or written */
|
|
if (pick(2))
|
|
return cond() | 0x010F0000 | (dst_reg() << 12); /* MRS rd, CPSR */
|
|
if (pick(2))
|
|
return cond() | 0x0128F000 | pick(13); /* MSR CPSR_f, rm */
|
|
return cond() | 0x0328F000 | (pick(16) << 8) | pick(256); /* MSR CPSR_f, #imm */
|
|
case 15: /* a branch forward, or branch and link */
|
|
{
|
|
const u32 skip = pick(3); /* over 0 to 2 instructions */
|
|
if (at + 2 + skip > count)
|
|
return 0xE1A00000; /* too near the end: mov r0, r0 */
|
|
return cond() | (5u << 25) | (pick(2) << 24) | skip;
|
|
}
|
|
}
|
|
}
|
|
|
|
static u64 fold(u64 h, const void *data, size_t size)
|
|
{
|
|
const u8 *p = (const u8*)data;
|
|
size_t i;
|
|
|
|
for (i = 0; i + 8 <= size; i += 8)
|
|
{
|
|
u64 v;
|
|
memcpy(&v, p + i, 8);
|
|
h = (h ^ v) * 0x100000001b3ull;
|
|
h ^= h >> 29;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static void run_case(unsigned number)
|
|
{
|
|
u32 program[64];
|
|
unsigned count, i;
|
|
|
|
seed = 0x9E3779B97F4A7C15ull * (number + 1);
|
|
memset(ram, 0, sizeof(ram));
|
|
count = 8 + pick(33);
|
|
for (i = 0; i < count; i++)
|
|
program[i] = instruction(i, count);
|
|
program[count] = 0xEAFFFFFE; /* b . */
|
|
if (getenv("ARM7_TEST_PROGRAM"))
|
|
{
|
|
FILE *f = fopen(getenv("ARM7_TEST_PROGRAM"), "r");
|
|
for (i = 0; i <= count; i++)
|
|
if (fscanf(f, "%x", &program[i]) != 1)
|
|
program[i] = 0xEAFFFFFE;
|
|
fclose(f);
|
|
}
|
|
if (getenv("ARM7_TEST_PRINT"))
|
|
{
|
|
for (i = 0; i <= count; i++)
|
|
fprintf(stderr, "%08x ", program[i]);
|
|
fprintf(stderr, "\n");
|
|
}
|
|
memcpy(&ram[PROGRAM], program, (count + 1) * 4);
|
|
*(u32*)&ram[0] = 0xEA000000 | ((PROGRAM - 8) / 4); /* the reset vector: b PROGRAM */
|
|
/* something to load */
|
|
for (i = 0x100000; i < 0x100000 + 0x10000; i += 4)
|
|
*(u32*)&ram[i] = rnd();
|
|
|
|
aicaarm::enable(false);
|
|
aicaarm::enable(true); /* which resets it */
|
|
for (i = 0; i < 13; i++)
|
|
arm_Reg[i].I = pick(4) ? 0x100000 + (pick(0x4000) << 2) + pick(4) : rnd();
|
|
arm_Reg[13].I = 0x180000;
|
|
|
|
if (setjmp(stopped))
|
|
{
|
|
printf("%u %u stopped on a check of its own\n", number, count);
|
|
return;
|
|
}
|
|
/* how the build with the recompiler in it is asked for the interpreter
|
|
* instead, as the core option does: for the whole program, or changing
|
|
* over and back while it runs */
|
|
if (getenv("ARM7_TEST_INTERPRET"))
|
|
{
|
|
settings.dynarec.Arm7Interpreter = true;
|
|
aicaarm::run(64);
|
|
settings.dynarec.Arm7Interpreter = false;
|
|
}
|
|
else if (getenv("ARM7_TEST_SWITCH"))
|
|
{
|
|
/* over the first samples, which is where a program this short
|
|
* does its work; every change back to the recompiler makes it start
|
|
* over, which takes time, so not for all 64 */
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
settings.dynarec.Arm7Interpreter = (number + i) & 1;
|
|
aicaarm::run(1);
|
|
}
|
|
settings.dynarec.Arm7Interpreter = false;
|
|
aicaarm::run(60);
|
|
}
|
|
else
|
|
aicaarm::run(64);
|
|
|
|
if (memcmp(&ram[PROGRAM], program, (count + 1) * 4) || *(u32*)&ram[0] != (0xEA000000 | ((PROGRAM - 8) / 4)))
|
|
{
|
|
printf("%u %u wrote over its own instructions: not compared\n", number, count);
|
|
return;
|
|
}
|
|
CPUUpdateCPSR();
|
|
printf("%u %u", number, count);
|
|
for (i = 0; i < 15; i++)
|
|
printf(" %08x", arm_Reg[i].I);
|
|
printf(" pc %08x cpsr %08x mem %016llx\n", arm_Reg[R15_ARM_NEXT].I, arm_Reg[RN_CPSR].I,
|
|
(unsigned long long)fold(0xcbf29ce484222325ull, ram, sizeof(ram)));
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const unsigned cases = argc > 1 ? (unsigned)atoi(argv[1]) : 1000;
|
|
unsigned i;
|
|
|
|
modes = getenv("ARM7_TEST_MODES") != NULL;
|
|
aica_ram.data = ram;
|
|
aica_ram.size = sizeof(ram);
|
|
aicaarm::init();
|
|
if (getenv("ARM7_TEST_CASE"))
|
|
run_case((unsigned)atoi(getenv("ARM7_TEST_CASE")));
|
|
else
|
|
for (i = argc > 2 ? (unsigned)atoi(argv[2]) : 0; i < cases; i++)
|
|
run_case(i);
|
|
return 0;
|
|
}
|