diff --git a/src/app/pipeline.c b/src/app/pipeline.c index dfb421b..8e3107f 100644 --- a/src/app/pipeline.c +++ b/src/app/pipeline.c @@ -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); @@ -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); diff --git a/src/backend/emitter.c b/src/backend/emitter.c index 809cb82..f7c49e8 100644 --- a/src/backend/emitter.c +++ b/src/backend/emitter.c @@ -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_() 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) { @@ -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"); } @@ -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" @@ -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; @@ -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; diff --git a/src/backend/emitter.h b/src/backend/emitter.h index 1390624..17a4a5f 100644 --- a/src/backend/emitter.h +++ b/src/backend/emitter.h @@ -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_() 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); diff --git a/src/cpu/cpu.c b/src/cpu/cpu.c index 3054035..1248cae 100644 --- a/src/cpu/cpu.c +++ b/src/cpu/cpu.c @@ -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;