Skip to content
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
32 changes: 32 additions & 0 deletions src/app/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -1015,10 +1015,40 @@ int emit_code_sections_split(const LoadedCodeSection* sections,
file_count++;
}

// Hand the emitter every chunk entry known so far so a cross-chunk bl
// can be emitted as a direct call. funcs accumulates across sections and
// this section's chunks were just added, so a target in this or any
// earlier section resolves; one in a later section does not yet exist as
// a symbol and falls back to the return-to-chassis form. That costs
// almost nothing in practice -- .text1 holds 181 of this title's 182
// chunks, so nearly every call is intra-section.
// emitter.c calls "no chunk table" the escape hatch if direct calls
// misbehave, but only this function could set it, so there was no way to
// emit a module *without* the calls. -DDOLRECOMP_C_MAX_CALL_DEPTH=0
// stops the path being taken, which is the right switch for a module
// already built, but it leaves every call site and guard branch in the
// generated C -- so it cannot answer what the feature costs when it is
// absent. Same idiom as DOLRECOMP_C_CHUNK_INSTRUCTIONS above.
const char* no_direct = getenv("DOLRECOMP_NO_DIRECT_CALLS");
u32* chunk_starts = NULL;
if (no_direct && *no_direct && *no_direct != '0') {
printf(" cross-chunk direct calls disabled "
"(DOLRECOMP_NO_DIRECT_CALLS)\n");
} else {
chunk_starts = (u32*)malloc((size_t)funcs.count * sizeof(u32));
if (chunk_starts) {
for (u32 i = 0; i < funcs.count; ++i)
chunk_starts[i] = funcs.ranges[i].start;
emit_set_chunk_table(chunk_starts, funcs.count);
}
}

u32 active_jobs = effective_chunk_jobs(section_job_count, jobs);
printf(" writing %u chunks with %u job%s\n",
section_job_count, active_jobs, active_jobs == 1 ? "" : "s");
if (!run_chunk_jobs(chunk_jobs, section_job_count, jobs)) {
emit_set_chunk_table(NULL, 0);
free(chunk_starts);
smc_analysis_free(&smc);
function_list_free(&funcs);
free(chunk_jobs);
Expand All @@ -1027,6 +1057,8 @@ int emit_code_sections_split(const LoadedCodeSection* sections,
fclose(manifest);
return 0;
}
emit_set_chunk_table(NULL, 0);
free(chunk_starts);

free(chunk_jobs);
free(insts);
Expand Down
95 changes: 91 additions & 4 deletions src/backend/emitter.c
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,73 @@ static bool branch_target_is_local(u32 func_start, u32 func_end, u32 target) {
return target >= func_start && target < func_end && ((target - func_start) & 3u) == 0;
}

// Chunk entry addresses, sorted. Written once before the worker pool starts and
// read-only thereafter, so no locking. NULL means "no table": every cross-chunk
// branch falls back to the return-to-chassis form, which is what shipped before
// direct calls existed and remains the escape hatch if they misbehave.
static const u32* g_chunk_starts = NULL;
static u32 g_chunk_count = 0;

void emit_set_chunk_table(const u32* starts, u32 count) {
g_chunk_starts = count ? starts : NULL;
g_chunk_count = count ? count : 0;
}

// The chunk whose func_<start>() covers `addr`, or 0 if none does. Chunks tile
// the text sections but the first one does not start on the common stride, so
// this binary-searches rather than dividing.
static u32 chunk_start_for(u32 addr) {
if (!g_chunk_starts || !g_chunk_count)
return 0;
u32 lo = 0, hi = g_chunk_count;
while (lo < hi) {
u32 mid = lo + (hi - lo) / 2u;
if (g_chunk_starts[mid] <= addr)
lo = mid + 1u;
else
hi = mid;
}
return lo ? g_chunk_starts[lo - 1u] : 0;
}


// A cross-chunk `bl` whose target chunk is known: call it directly instead of
// returning to the chassis. The chassis round trip costs two rel-section scans,
// two IsHostCallAddress hash lookups, a ModManager dispatch and a downcount
// flush, none of which a same-module call needs.
//
// Resume inline only if the callee came back to the instruction after the call.
// Any other pc means it stopped early -- budget exhausted, an exception, a
// tail-call elsewhere -- and only the chassis knows what to do next.
//
// The prototype is declared at block scope so this needs no header plumbing;
// the definition lives in another translation unit and the linker resolves it.
static bool emit_cross_chunk_call(FILE* out, const PPCInst* inst,
u32 func_start, u32 func_end) {
u32 continuation = inst->address + 4u;
u32 target_chunk = chunk_start_for(inst->branch_target);
if (!target_chunk)
return false;
// Without a local continuation label there is nothing to resume into, so
// the call would buy nothing over the plain return.
if (!branch_target_is_local(func_start, func_end, continuation))
return false;

fprintf(out, " ctx->pc = 0x%08Xu;\n", inst->branch_target);
fprintf(out, " if (dolrecomp_call_enter()) {\n");
fprintf(out, " void func_%08X(CPUState* ctx);\n", target_chunk);
fprintf(out, " func_%08X(ctx);\n", target_chunk);
fprintf(out, " dolrecomp_call_leave();\n");
fprintf(out, " if (ctx->pc == 0x%08Xu) goto label_%08X;\n",
continuation, continuation);
fprintf(out, " }\n");
fprintf(out, " return;\n");
return true;
}

static void emit_direct_branch(FILE* out, const PPCInst* inst,
bool local_target, bool direct_backedge) {
bool local_target, bool direct_backedge,
u32 func_start, u32 func_end) {
bool local_backward = local_target && inst->branch_target <= inst->address;

if (inst->lk) {
Expand All @@ -327,7 +391,7 @@ static void emit_direct_branch(FILE* out, const PPCInst* inst,
fprintf(out, " }\n");
}
fprintf(out, " goto label_%08X;\n", inst->branch_target);
} else {
} else if (!emit_cross_chunk_call(out, inst, func_start, func_end)) {
fprintf(out, " ctx->pc = 0x%08Xu;\n", inst->branch_target);
fprintf(out, " return;\n");
}
Expand Down Expand Up @@ -434,6 +498,29 @@ void emit_header_for_cpu(FILE* out, DolRecompCPU cpu) {
"#define DOLRECOMP_C_LOOP_CYCLE_BUDGET 256\n"
"#endif\n"
"\n"
"/* Cross-chunk calls turn guest recursion into host recursion, and a\n"
" chunk frame is not small. Without a ceiling a deep guest call chain\n"
" overflows the host stack, which is a crash rather than a slow\n"
" emulator. Past the limit the call site falls back to returning to\n"
" the chassis, which is always correct -- ctx->pc already names the\n"
" target, so the chassis simply dispatches it as it did before.\n"
" The counter is plain static, not atomic: the chassis runs the module\n"
" on one CPU thread. */\n"
"#ifndef DOLRECOMP_C_MAX_CALL_DEPTH\n"
"#define DOLRECOMP_C_MAX_CALL_DEPTH 24\n"
"#endif\n"
"extern unsigned dolrecomp_call_depth;\n"
"static inline int dolrecomp_call_enter(void) {\n"
" if (dolrecomp_call_depth >= (unsigned)DOLRECOMP_C_MAX_CALL_DEPTH)\n"
" return 0;\n"
" dolrecomp_call_depth++;\n"
" return 1;\n"
"}\n"
"static inline void dolrecomp_call_leave(void) {\n"
" if (dolrecomp_call_depth)\n"
" dolrecomp_call_depth--;\n"
"}\n"
"\n"
"static inline u32 dolrecomp_rotl32(u32 value, u32 sh) {\n"
" sh &= 31u;\n"
" return sh ? ((value << sh) | (value >> (32u - sh))) : value;\n"
Expand Down Expand Up @@ -1559,7 +1646,7 @@ static void emit_instruction_with_range(FILE* out, const PPCInst* inst,
fprintf(out, " {\n");
emit_direct_branch(out, inst,
branch_target_is_local(func_start, func_end, inst->branch_target),
direct_backedge);
direct_backedge, func_start, func_end);
fprintf(out, " }\n");
break;

Expand All @@ -1569,7 +1656,7 @@ static void emit_instruction_with_range(FILE* out, const PPCInst* inst,
fprintf(out, " if (ctr_ok && cr_ok) {\n");
emit_direct_branch(out, inst,
branch_target_is_local(func_start, func_end, inst->branch_target),
direct_backedge);
direct_backedge, func_start, func_end);
fprintf(out, " }\n");
fprintf(out, " }\n");
break;
Expand Down
7 changes: 7 additions & 0 deletions src/backend/emitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ typedef enum {
void emit_header(FILE* out);
void emit_header_for_cpu(FILE* out, DolRecompCPU cpu);

// Register the chunk entry addresses so a cross-chunk `bl` can be emitted as a
// direct call to func_<chunk>() instead of a return to the chassis. Must be
// called before emission starts: chunk files are emitted on a worker pool and
// the table is read-only from then on. Passing count == 0 disables direct calls
// and restores the return-to-chassis form.
void emit_set_chunk_table(const u32* starts, u32 count);

// emit a single recompiled function as C code
bool emit_function(FILE* out, const PPCInst* insts, u32 count, u32 func_addr);

Expand Down
8 changes: 8 additions & 0 deletions src/cpu/cpu.c
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,14 @@ void ppc_program_exception(CPUState* cpu, u32 cause, u32 cia) {
ppc_take_exception(cpu, PPC_EXC_PROGRAM, PPC_VECTOR_PROGRAM, cia, cause);
}

/* Depth of the generated code's native call chain. Cross-chunk direct calls
turn guest recursion into host recursion, and the chunk headers declare this
extern so all ~180 chunk translation units share one counter -- as a static
in the header each would get its own and the guard would bound nothing.
It lives here because the C backend compiles only chunk source files, so generated.c
is not linked and cannot hold the definition. */
unsigned dolrecomp_call_depth = 0;

bool ppc_fp_available(CPUState* cpu, u32 cia) {
if (cpu->msr & PPC_MSR_FP)
return true;
Expand Down
Loading