diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml index 8689e8f..f48c6f6 100644 --- a/.github/workflows/cmake-single-platform.yml +++ b/.github/workflows/cmake-single-platform.yml @@ -49,3 +49,78 @@ jobs: - name: Test working-directory: ${{ github.workspace }}/build run: ctest --build-config ${{ matrix.build_type }} --output-on-failure + + # Exercise the LLVM backend and both supported object formats. + llvm: + name: LLVM backend (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install LLVM (Linux) + if: runner.os == 'Linux' + run: | + curl -fsSL -o llvm.sh https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 20 + sudo apt-get install -y llvm-20-dev + + - name: Install Windows toolchain + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-ninja + + - name: Install LLVM (Windows) + if: runner.os == 'Windows' + shell: msys2 {0} + run: > + pacman --noconfirm -U + https://repo.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-llvm-20-20.1.8-4-any.pkg.tar.zst + + - name: Configure (Linux) + if: runner.os == 'Linux' + run: > + cmake -B ${{ github.workspace }}/build -S ${{ github.workspace }} + -DCMAKE_BUILD_TYPE=Release + -DDOLRECOMP_ENABLE_LLVM=ON + -DLLVM_DIR=/usr/lib/llvm-20/lib/cmake/llvm + + - name: Configure (Windows) + if: runner.os == 'Windows' + shell: msys2 {0} + run: > + cmake -G Ninja -B build -S . + -DCMAKE_BUILD_TYPE=Release + -DDOLRECOMP_ENABLE_LLVM=ON + -DLLVM_DIR=/ucrt64/opt/llvm-20/lib/cmake/llvm + + - name: Build (Linux) + if: runner.os == 'Linux' + run: cmake --build ${{ github.workspace }}/build --config Release + + - name: Build (Windows) + if: runner.os == 'Windows' + shell: msys2 {0} + run: cmake --build build + + - name: Test (Linux) + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }}/build + run: ctest --build-config Release --output-on-failure + + - name: Test (Windows) + if: runner.os == 'Windows' + shell: msys2 {0} + run: ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f41c9d..73ead32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,17 @@ set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) option(DOLRECOMP_ENABLE_LLVM "Build the x86-64 LLVM object backend" OFF) +# Discover LLVM before creating targets so its MSVC runtime choice is uniform. +if(DOLRECOMP_ENABLE_LLVM) + enable_language(CXX) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + find_package(LLVM CONFIG REQUIRED) + if(LLVM_PACKAGE_VERSION VERSION_LESS 19 OR LLVM_PACKAGE_VERSION VERSION_GREATER_EQUAL 21) + message(FATAL_ERROR "DolRecomp LLVM backend requires LLVM 19 or 20 (found ${LLVM_PACKAGE_VERSION})") + endif() +endif() + # Portable warnings for local development and CI. if(MSVC) add_compile_options(/W3) @@ -77,13 +88,6 @@ target_include_directories(dr_ir PUBLIC ${DOLRECOMP_SRC}) target_link_libraries(dr_ir PUBLIC dr_frontend) if(DOLRECOMP_ENABLE_LLVM) - enable_language(CXX) - set(CMAKE_CXX_STANDARD 17) - set(CMAKE_CXX_STANDARD_REQUIRED ON) - find_package(LLVM CONFIG REQUIRED) - if(LLVM_PACKAGE_VERSION VERSION_LESS 19 OR LLVM_PACKAGE_VERSION VERSION_GREATER_EQUAL 21) - message(FATAL_ERROR "DolRecomp LLVM backend requires LLVM 19 or 20 (found ${LLVM_PACKAGE_VERSION})") - endif() add_library(dr_llvm STATIC src/backend/llvm/llvm_backend.cpp src/backend/llvm/llvm_control_flow.cpp @@ -101,14 +105,14 @@ if(DOLRECOMP_ENABLE_LLVM) NAMES LLVM-${LLVM_VERSION_MAJOR} LLVM HINTS ${LLVM_LIBRARY_DIRS} NO_DEFAULT_PATH) - if(NOT DOLRECOMP_LLVM_SHARED_IMPORT OR - NOT DOLRECOMP_LLVM_SHARED_IMPORT MATCHES "[.]dll[.]a$") - message(FATAL_ERROR - "DolRecomp requires LLVM's shared import library on MinGW; " - "install the matching MSYS2 llvm-libs package") + if(DOLRECOMP_LLVM_SHARED_IMPORT MATCHES "[.]dll[.]a$") + target_link_libraries(dr_llvm PRIVATE + "${DOLRECOMP_LLVM_SHARED_IMPORT}") + else() + llvm_map_components_to_libnames(DOLRECOMP_LLVM_LIBS + Core Support Analysis Passes Target MC native nativecodegen) + target_link_libraries(dr_llvm PRIVATE ${DOLRECOMP_LLVM_LIBS}) endif() - target_link_libraries(dr_llvm PRIVATE - "${DOLRECOMP_LLVM_SHARED_IMPORT}") elseif(TARGET LLVM) target_link_libraries(dr_llvm PRIVATE LLVM) else() @@ -156,6 +160,10 @@ add_executable(dolrecomp src/app/main.c) target_link_libraries(dolrecomp PRIVATE dr_app) if(DOLRECOMP_ENABLE_LLVM) set_property(TARGET dolrecomp PROPERTY LINKER_LANGUAGE CXX) + # Large LLVM modules exceed MSVC's default 1 MiB stack. + if(MSVC) + target_link_options(dolrecomp PRIVATE /STACK:8388608) + endif() endif() add_executable(dolir_stats tools/dolir_stats.c) diff --git a/src/app/pipeline.c b/src/app/pipeline.c index 9f623a3..dfb421b 100644 --- a/src/app/pipeline.c +++ b/src/app/pipeline.c @@ -53,7 +53,8 @@ static u32 c_chunk_instructions(void) { #ifdef DOLRECOMP_ENABLE_LLVM #define DOLLLVM_DEFAULT_CHUNK_INSTRUCTIONS 1024u #define DOLLLVM_DEFAULT_WORKER_BATCH 4u -#define DOLLLVM_CACHE_VERSION "dolllvm-v4" +// v6 carries the execution budget across generated function calls. +#define DOLLLVM_CACHE_VERSION "dolllvm-v6" typedef struct { const PPCInst* insts; @@ -103,16 +104,11 @@ static u32 llvm_worker_batch_size(void) { return (u32)value; } +// Validate the object format selected by the target triple. static int valid_object_file(const char* path) { - FILE* file = fopen(path, "rb"); - if (!file) - return 0; - unsigned char magic[4]; - int valid = fread(magic, 1, sizeof(magic), file) == sizeof(magic) && - magic[0] == 0x7Fu && magic[1] == 'E' && - magic[2] == 'L' && magic[3] == 'F'; - fclose(file); - return valid; + return dolllvm_object_matches_triple(path, getenv("DOLRECOMP_LLVM_TARGET")) + ? 1 + : 0; } static int llvm_job_stamp_path(const LLVMChunkJob* job, char* path, @@ -206,9 +202,11 @@ static u64 llvm_job_hash(const LLVMChunkJob* job) { hash = hash_bytes(hash, &job->count, sizeof(job->count)); u32 state_size = (u32)sizeof(CPUState); hash = hash_bytes(hash, &state_size, sizeof(state_size)); - const char* target = getenv("DOLRECOMP_LLVM_TARGET"); - if (target) - hash = hash_bytes(hash, target, strlen(target)); + // Host triples must distinguish caches when no target was requested. + char triple[256]; + if (dolllvm_effective_triple(getenv("DOLRECOMP_LLVM_TARGET"), triple, + sizeof(triple))) + hash = hash_bytes(hash, triple, strlen(triple)); for (u32 i = 0; i < job->count; i++) { hash = hash_bytes(hash, &job->insts[i].address, sizeof(job->insts[i].address)); diff --git a/src/backend/llvm/llvm_backend.cpp b/src/backend/llvm/llvm_backend.cpp index 79817f9..05ecf2f 100644 --- a/src/backend/llvm/llvm_backend.cpp +++ b/src/backend/llvm/llvm_backend.cpp @@ -28,6 +28,12 @@ namespace { using namespace llvm; +// Shared by emission, cache hashing and resume validation. +static std::string resolveTriple(const char *requested) { + return requested && requested[0] ? std::string(requested) + : llvm::sys::getDefaultTargetTriple(); +} + static CodeGenOptLevel codegenLevel(int level) { if (level <= 0) return CodeGenOptLevel::None; @@ -72,9 +78,7 @@ extern "C" bool dolllvm_emit_object(const DolIRModule *source, int opt = options ? options->optimization_level : 2; std::string tripleName = - options && options->target_triple && options->target_triple[0] - ? options->target_triple - : llvm::sys::getDefaultTargetTriple(); + resolveTriple(options ? options->target_triple : nullptr); const llvm::Triple triple(tripleName); if (triple.getArch() != llvm::Triple::x86_64 || (!triple.isOSLinux() && !triple.isOSWindows())) { @@ -133,7 +137,14 @@ extern "C" bool dolllvm_emit_object(const DolIRModule *source, passBuilder.crossRegisterProxies(lam, fam, cgam, mam); llvm::ModulePassManager passes; std::string pipeline = - "function(mem2reg,early-cse,instcombine,simplifycfg,sccp," + // instcombine's fixpoint check is a self-diagnostic for the pass, not a + // correctness property of the IR. Recompiled Gekko functions contain + // long straight-line integer and condition-flag sequences that can still + // be changing after one iteration, which makes the pass call + // report_fatal_error and take the whole recompilation down. Suppressing + // the check leaves the optimization itself intact. + "function(mem2reg,early-cse,instcombine," + "simplifycfg,sccp," "correlated-propagation,jump-threading,gvn,dse,adce,loop-simplify," "loop-rotate,loop-mssa(licm),loop-vectorize,slp-vectorizer,vector-" "combine," @@ -182,3 +193,36 @@ extern "C" bool dolllvm_emit_object(const DolIRModule *source, objectFile.flush(); return true; } + +extern "C" bool dolllvm_effective_triple(const char *requested, char *out, + size_t size) { + if (!out || size == 0) + return false; + const std::string triple = resolveTriple(requested); + if (triple.size() + 1 > size) + return false; + memcpy(out, triple.c_str(), triple.size() + 1); + return true; +} + +extern "C" bool dolllvm_object_matches_triple(const char *path, + const char *requested) { + FILE *file = fopen(path, "rb"); + if (!file) + return false; + unsigned char magic[4] = {0, 0, 0, 0}; + const size_t read = fread(magic, 1, sizeof(magic), file); + fclose(file); + if (read != sizeof(magic)) + return false; + + const llvm::Triple triple(resolveTriple(requested)); + if (triple.isOSBinFormatCOFF()) + // IMAGE_FILE_MACHINE_AMD64, little-endian, at offset 0 of a COFF object. + return magic[0] == 0x64 && magic[1] == 0x86; + if (triple.isOSBinFormatMachO()) + return magic[0] == 0xCF && magic[1] == 0xFA && magic[2] == 0xED && + magic[3] == 0xFE; + return magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && + magic[3] == 'F'; +} diff --git a/src/backend/llvm/llvm_backend.h b/src/backend/llvm/llvm_backend.h index f5949a9..51743e2 100644 --- a/src/backend/llvm/llvm_backend.h +++ b/src/backend/llvm/llvm_backend.h @@ -25,6 +25,12 @@ typedef struct { bool dolllvm_emit_object(const DolIRModule* module, const char* object_path, const DolLLVMOptions* options, FILE* diagnostics); +// Resolve an optional target to the triple used for emission. +bool dolllvm_effective_triple(const char* requested, char* out, size_t size); + +// Validate an object's magic against the effective target triple. +bool dolllvm_object_matches_triple(const char* path, const char* requested); + #ifdef __cplusplus } #endif diff --git a/src/backend/llvm/llvm_control_flow.cpp b/src/backend/llvm/llvm_control_flow.cpp index e4fec63..745768a 100644 --- a/src/backend/llvm/llvm_control_flow.cpp +++ b/src/backend/llvm/llvm_control_flow.cpp @@ -37,13 +37,20 @@ BasicBlock *FunctionEmitter::externalDestination(const DolIRTerminator &term, context_, term.linked ? "direct_call" : "direct_tail", function_); IRBuilderBase::InsertPoint saved = builder_.saveIP(); builder_.SetInsertPoint(callBlock); + emitBudgetGuard(target); materialize(target); char name[64]; - snprintf(name, sizeof(name), "func_%08X", range->start); + snprintf(name, sizeof(name), "func_%08X_budget", range->start); auto callee = module_.getOrInsertFunction( name, FunctionType::get(Type::getVoidTy(context_), - {PointerType::getUnqual(context_)}, false)); - builder_.CreateCall(callee, {ctx_}); + {PointerType::getUnqual(context_), + PointerType::getUnqual(context_), + PointerType::getUnqual(context_)}, false)); + if (auto *calleeFunction = dyn_cast(callee.getCallee())) { + calleeFunction->setVisibility(GlobalValue::HiddenVisibility); + calleeFunction->setDSOLocal(true); + } + builder_.CreateCall(callee, {ctx_, guard_cycles_, guard_steps_}); if (!term.linked) { builder_.CreateRetVoid(); builder_.restoreIP(saved); diff --git a/src/backend/llvm/llvm_function_emitter.cpp b/src/backend/llvm/llvm_function_emitter.cpp index 8ac72e0..a7cc89b 100644 --- a/src/backend/llvm/llvm_function_emitter.cpp +++ b/src/backend/llvm/llvm_function_emitter.cpp @@ -24,15 +24,28 @@ FunctionEmitter::FunctionEmitter(LLVMContext &context, Module &module, ranges_(ranges), range_count_(range_count) {} bool FunctionEmitter::emit(raw_ostream &diagnostics) { + auto *pointer = PointerType::getUnqual(context_); auto *type = FunctionType::get(Type::getVoidTy(context_), - {PointerType::getUnqual(context_)}, false); - function_ = Function::Create(type, GlobalValue::ExternalLinkage, source_.name, - module_); + {pointer, pointer, pointer}, false); + const std::string bodyName = std::string(source_.name) + "_budget"; + function_ = module_.getFunction(bodyName); + if (!function_) + function_ = Function::Create(type, GlobalValue::ExternalLinkage, bodyName, + module_); + if (function_->getFunctionType() != type || !function_->empty()) { + diagnostics << "dolllvm: conflicting native body " << bodyName << "\n"; + return false; + } function_->setCallingConv(CallingConv::C); function_->setVisibility(GlobalValue::HiddenVisibility); function_->setDSOLocal(true); + function_->addFnAttr(Attribute::NoInline); ctx_ = function_->getArg(0); ctx_->setName("ctx"); + guard_cycles_ = function_->getArg(1); + guard_cycles_->setName("guard_cycles"); + guard_steps_ = function_->getArg(2); + guard_steps_->setName("guard_steps"); entry_ = BasicBlock::Create(context_, "entry", function_); for (u32 i = 0; i < source_.block_count; i++) @@ -44,7 +57,38 @@ bool FunctionEmitter::emit(raw_ostream &diagnostics) { for (u32 i = 0; i < source_.block_count; i++) if (!emitBlock(i, diagnostics)) return false; - return !verifyFunction(*function_, &diagnostics); + if (verifyFunction(*function_, &diagnostics)) + return false; + return emitWrapper(diagnostics); +} + +bool FunctionEmitter::emitWrapper(raw_ostream &diagnostics) { + auto *pointer = PointerType::getUnqual(context_); + auto *type = FunctionType::get(Type::getVoidTy(context_), {pointer}, false); + Function *wrapper = module_.getFunction(source_.name); + if (!wrapper) + wrapper = Function::Create(type, GlobalValue::ExternalLinkage, source_.name, + module_); + if (wrapper->getFunctionType() != type || !wrapper->empty()) { + diagnostics << "dolllvm: conflicting native entry " << source_.name << "\n"; + return false; + } + wrapper->setCallingConv(CallingConv::C); + wrapper->setVisibility(GlobalValue::HiddenVisibility); + wrapper->setDSOLocal(true); + wrapper->getArg(0)->setName("ctx"); + + BasicBlock *entry = BasicBlock::Create(context_, "entry", wrapper); + IRBuilder<> builder(entry); + AllocaInst *guardCycles = + builder.CreateAlloca(Type::getInt64Ty(context_), nullptr, "guard_cycles"); + AllocaInst *guardSteps = + builder.CreateAlloca(Type::getInt64Ty(context_), nullptr, "guard_steps"); + builder.CreateStore(builder.getInt64(0), guardCycles); + builder.CreateStore(builder.getInt64(0), guardSteps); + builder.CreateCall(function_, {wrapper->getArg(0), guardCycles, guardSteps}); + builder.CreateRetVoid(); + return !verifyFunction(*wrapper, &diagnostics); } std::string FunctionEmitter::blockName(u32 index) const { @@ -352,6 +396,13 @@ void FunctionEmitter::chargeCycles(u32 cycles) { Value *next = builder_.CreateAdd( old, ConstantInt::get(Type::getInt64Ty(context_), cycles)); builder_.CreateStore(next, cycles_); + // The shared guard survives helper and generated-function boundaries. + Value *guard_old = + builder_.CreateLoad(Type::getInt64Ty(context_), guard_cycles_); + builder_.CreateStore( + builder_.CreateAdd(guard_old, + ConstantInt::get(Type::getInt64Ty(context_), cycles)), + guard_cycles_); } void FunctionEmitter::materialize(u32 pc) { @@ -367,6 +418,7 @@ void FunctionEmitter::materialize(u32 pc) { ConstantInt::get(Type::getInt32Ty(context_), pc)); Value *downcount = loadOffset(Type::getInt64Ty(context_), offsetof(CPUState, downcount)); + // Only unmaterialized cycles are owed to downcount. Value *cycles = builder_.CreateLoad(Type::getInt64Ty(context_), cycles_); builder_.CreateStore(builder_.CreateSub(downcount, cycles), bytePtr(offsetof(CPUState, downcount))); @@ -378,9 +430,19 @@ void FunctionEmitter::sideExit(u32 pc) { } void FunctionEmitter::emitBudgetGuard(u32 pc) { - Value *cycles = builder_.CreateLoad(Type::getInt64Ty(context_), cycles_); - Value *exhausted = builder_.CreateICmpUGE( + // Guard the whole native call chain, not one generated function. + Value *cycles = + builder_.CreateLoad(Type::getInt64Ty(context_), guard_cycles_); + Value *over_cycles = builder_.CreateICmpUGE( cycles, ConstantInt::get(Type::getInt64Ty(context_), 256)); + // Backstop for zero-cycle loops. + Value *steps = builder_.CreateLoad(Type::getInt64Ty(context_), guard_steps_); + Value *next_steps = builder_.CreateAdd( + steps, ConstantInt::get(Type::getInt64Ty(context_), 1)); + builder_.CreateStore(next_steps, guard_steps_); + Value *over_steps = builder_.CreateICmpUGE( + next_steps, ConstantInt::get(Type::getInt64Ty(context_), 2048)); + Value *exhausted = builder_.CreateOr(over_cycles, over_steps); BasicBlock *run = BasicBlock::Create(context_, "budget_run", function_); BasicBlock *exit = BasicBlock::Create(context_, "budget_exit", function_); builder_.CreateCondBr(exhausted, exit, run); diff --git a/src/backend/llvm/llvm_function_emitter.h b/src/backend/llvm/llvm_function_emitter.h index 7b1d69c..cebaaae 100644 --- a/src/backend/llvm/llvm_function_emitter.h +++ b/src/backend/llvm/llvm_function_emitter.h @@ -49,6 +49,7 @@ class FunctionEmitter final { void scanLoopHeaders(); void emitEntry(); + bool emitWrapper(llvm::raw_ostream &diagnostics); void chargeCycles(u32 cycles); void materialize(u32 pc); void sideExit(u32 pc); @@ -111,6 +112,10 @@ class FunctionEmitter final { llvm::Argument *ctx_ = nullptr; llvm::BasicBlock *entry_ = nullptr; llvm::AllocaInst *cycles_ = nullptr; + // Shared across generated calls until control returns to the dispatcher. + llvm::Value *guard_cycles_ = nullptr; + // Termination backstop for zero-cycle loops. + llvm::Value *guard_steps_ = nullptr; std::array state_{}; std::array used_{}; std::array dirty_{}; diff --git a/tests/test_llvm_backend.cpp b/tests/test_llvm_backend.cpp index e729aa1..adffe9e 100644 --- a/tests/test_llvm_backend.cpp +++ b/tests/test_llvm_backend.cpp @@ -103,19 +103,44 @@ int main(int argc, char** argv) { }; CHECK(add_chunk(&module, paired_words, 7, 0x80002B00u)); + // Runtime boundaries must not reset the dispatcher budget. + const u32 budget_words[] = { + 0x38630001u, 0x00000000u, 0x2C032710u, 0x4180FFF4u, 0x4E800020u, + }; + CHECK(add_chunk(&module, budget_words, 5, 0x80002C00u)); + + // External tail branches share the same budget across chunks. + const u32 cross_chunk_a[] = {0x48000100u}; + const u32 cross_chunk_b[] = {0x4BFFFF00u}; + CHECK(add_chunk(&module, cross_chunk_a, 1, 0x80002D00u)); + CHECK(add_chunk(&module, cross_chunk_b, 1, 0x80002E00u)); + CHECK(dolir_verify(&module, stderr)); DolLLVMOptions options{}; options.optimization_level = 2; options.verify = 1; options.emit_ir = 1; options.ir_path = argv[2]; + const DolLLVMFunctionRange ranges[] = { + {0x80002D00u, 0x80002D04u}, + {0x80002E00u, 0x80002E04u}, + }; + options.function_ranges = ranges; + options.function_range_count = 2; CHECK(dolllvm_emit_object(&module, argv[1], &options, stderr)); FILE* object = std::fopen(argv[1], "rb"); CHECK(object != nullptr); unsigned char magic[4]{}; CHECK(std::fread(magic, 1, sizeof(magic), object) == sizeof(magic)); std::fclose(object); + // The object format follows the default target triple, so this cannot assume + // ELF: a Windows host emits COFF, whose x86-64 objects begin with the machine + // type IMAGE_FILE_MACHINE_AMD64 (0x8664) stored little-endian. +#if defined(_WIN32) + CHECK(magic[0] == 0x64 && magic[1] == 0x86); +#else CHECK(magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'); +#endif std::ifstream ir(argv[2]); const std::string irText((std::istreambuf_iterator(ir)), std::istreambuf_iterator()); diff --git a/tests/test_llvm_execute.c b/tests/test_llvm_execute.c index de5831b..3c938b4 100644 --- a/tests/test_llvm_execute.c +++ b/tests/test_llvm_execute.c @@ -16,6 +16,8 @@ void func_80002800(CPUState* cpu); void func_80002900(CPUState* cpu); void func_80002A00(CPUState* cpu); void func_80002B00(CPUState* cpu); +void func_80002C00(CPUState* cpu); +void func_80002D00(CPUState* cpu); static u32 fallback_count; static int fallback_bad; @@ -250,6 +252,33 @@ int main(void) { CHECK(cpu.fpr[4] == cpu.fpr[5] && cpu.ps1[4] == cpu.ps1[6]); CHECK(fallback_count == 1); + + // A fallback in the loop must not restart the dispatcher budget. + prepare_call(&cpu, 0x80002C00u); + cpu.gpr[3] = 0; + cpu.downcount = 0; + fallback_count = 0; + func_80002C00(&cpu); + fprintf(stderr, "budget guard: %u iterations, downcount %lld\n", + cpu.gpr[3], (long long)cpu.downcount); + CHECK(cpu.gpr[3] > 0); + CHECK(cpu.gpr[3] < 1000); + CHECK(cpu.pc >= 0x80002C00u && cpu.pc <= 0x80002C10u); + CHECK(cpu.pc != cpu.lr); + CHECK(cpu.downcount < 0 && cpu.downcount > -1024); + CHECK(fallback_count == (u32)cpu.gpr[3]); + + // Re-entry starts a new budget and continues from the saved PC. + const u32 first_pass = cpu.gpr[3]; + func_80002C00(&cpu); + CHECK(cpu.gpr[3] > first_pass); + + prepare_call(&cpu, 0x80002D00u); + cpu.downcount = 0; + func_80002D00(&cpu); + CHECK(cpu.pc == 0x80002D00u || cpu.pc == 0x80002E00u); + CHECK(cpu.downcount <= -128 && cpu.downcount >= -512); + cpu_free(&cpu); return 0; } diff --git a/tests/test_llvm_pipeline.c b/tests/test_llvm_pipeline.c index 7fb7025..9ecb7a4 100644 --- a/tests/test_llvm_pipeline.c +++ b/tests/test_llvm_pipeline.c @@ -5,14 +5,35 @@ #include #include #include + +#if defined(_WIN32) +#include +#include +#else #include #include +#endif #define CHECK(x) do { if (!(x)) { fprintf(stderr, "check failed: %s:%d: %s\n", \ __FILE__, __LINE__, #x); return 1; } } while (0) static int make_dir(const char* path) { +#if defined(_WIN32) + return _mkdir(path) == 0 || errno == EEXIST; +#else return mkdir(path, 0777) == 0 || errno == EEXIST; +#endif +} + +// The emitted object format follows the default target triple, so this cannot +// assume ELF: a Windows host produces COFF, whose x86-64 objects start with the +// machine type IMAGE_FILE_MACHINE_AMD64 (0x8664) stored little-endian. +static int is_native_object(const u8* magic) { +#if defined(_WIN32) + return magic[0] == 0x64 && magic[1] == 0x86; +#else + return magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; +#endif } static int write_dol(const char* path) { @@ -55,6 +76,15 @@ int main(int argc, char** argv) { snprintf(second_object, sizeof(second_object), "%s/out/generated/chunks/chunk_0001_text0_80003900.o", argv[2]); CHECK(write_dol(dol)); +#if defined(_WIN32) + // Windows has no fork. _spawnl with _P_WAIT runs the child to completion and + // returns its exit status directly, and the child inherits this process's + // environment, so the chunk-size override is set here rather than between + // fork and exec. + CHECK(_putenv_s("DOLRECOMP_LLVM_CHUNK_INSTRUCTIONS", "512") == 0); + CHECK(_spawnl(_P_WAIT, argv[1], argv[1], "--gamecube", "--backend=llvm", + "-j2", dol, output, NULL) == 0); +#else pid_t child = fork(); CHECK(child >= 0); if (child == 0) { @@ -66,6 +96,7 @@ int main(int argc, char** argv) { int status = 0; CHECK(waitpid(child, &status, 0) == child); CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); +#endif FILE* file = fopen(header, "rb"); CHECK(file != NULL); char text[4096]; @@ -78,11 +109,11 @@ int main(int argc, char** argv) { u8 magic[4]; CHECK(fread(magic, 1, 4, file) == 4); fclose(file); - CHECK(magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'); + CHECK(is_native_object(magic)); file = fopen(second_object, "rb"); CHECK(file != NULL); CHECK(fread(magic, 1, 4, file) == 4); fclose(file); - CHECK(magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'); + CHECK(is_native_object(magic)); return 0; }