Skip to content
Merged
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
75 changes: 75 additions & 0 deletions .github/workflows/cmake-single-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 22 additions & 14 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 11 additions & 13 deletions src/app/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand Down
52 changes: 48 additions & 4 deletions src/backend/llvm/llvm_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())) {
Expand Down Expand Up @@ -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<memssa>,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<memssa>,instcombine<no-verify-fixpoint>,"
"simplifycfg,sccp,"
"correlated-propagation,jump-threading,gvn,dse,adce,loop-simplify,"
"loop-rotate,loop-mssa(licm),loop-vectorize,slp-vectorizer,vector-"
"combine,"
Expand Down Expand Up @@ -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';
}
6 changes: 6 additions & 0 deletions src/backend/llvm/llvm_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/backend/llvm/llvm_control_flow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Function>(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);
Expand Down
Loading
Loading