From e51a6d5fff0bbc14e1656c52adc19a4ab6ace23f Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Thu, 30 Jul 2026 21:11:32 +0100 Subject: [PATCH 1/2] perf(vm): allocate up to 16 script arguments and 64 local variables on the stack before falling back to heap takes cues from Purplesmaug's stack allocation of function args. Very early builds did this but hard-capped the local variables per code entry to 128 without any heap allocation fallback after that threshold, hamstringing compatibility. --- src/runner.c | 2 ++ src/vm.c | 57 ++++++++++++++++++++++++++++++++++++++++------------ src/vm.h | 13 +++++++++++- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/runner.c b/src/runner.c index c8e2524bc..0b65bfd12 100644 --- a/src/runner.c +++ b/src/runner.c @@ -215,6 +215,7 @@ static void executeCode(Runner* runner, Instance* instance, int32_t codeId) { const char* savedCodeName = vm->currentCodeName; RValue* savedLocalVars = vm->localVars; uint32_t savedLocalVarCount = vm->localVarCount; + bool savedLocalVarsOnHeap = vm->localVarsOnHeap; IntIntHashMap* savedCodeLocalsSlotMap = vm->currentCodeLocalsSlotMap; int32_t savedCodeIndex = vm->currentCodeIndex; int32_t savedStackTop = vm->stack.top; @@ -244,6 +245,7 @@ static void executeCode(Runner* runner, Instance* instance, int32_t codeId) { vm->currentCodeName = savedCodeName; vm->localVars = savedLocalVars; vm->localVarCount = savedLocalVarCount; + vm->localVarsOnHeap = savedLocalVarsOnHeap; vm->currentCodeLocalsSlotMap = savedCodeLocalsSlotMap; vm->currentCodeIndex = savedCodeIndex; vm->stack.top = savedStackTop; diff --git a/src/vm.c b/src/vm.c index 36ab05e67..a7e74a508 100644 --- a/src/vm.c +++ b/src/vm.c @@ -472,8 +472,9 @@ static uint32_t resolveLocalSlot(VMContext* ctx, int32_t varID) { if (slot >= ctx->localVarCount) { RValue* resizedLocalVars = (RValue *)safeCalloc(slot + 1, sizeof(RValue)); memcpy(resizedLocalVars, ctx->localVars, sizeof(RValue) * ctx->localVarCount); - free(ctx->localVars); + if (ctx->localVarsOnHeap) free(ctx->localVars); ctx->localVars = resizedLocalVars; + ctx->localVarsOnHeap = true; ctx->localVarCount = slot + 1; } return slot; @@ -648,9 +649,10 @@ void VM_writeToScriptArgs(VMContext* ctx, int32_t writeIndex, RValue val) { RValue* newScriptArgs = (RValue *)safeCalloc(writeIndex + 1, sizeof(RValue)); if (ctx->scriptArgCount > 0) { memcpy(newScriptArgs, ctx->scriptArgs, ctx->scriptArgCount * sizeof(RValue)); - free(ctx->scriptArgs); + if (ctx->scriptArgsOnHeap) free(ctx->scriptArgs); } ctx->scriptArgs = newScriptArgs; + ctx->scriptArgsOnHeap = true; ctx->scriptArgCount = writeIndex + 1; } RValue_free(&ctx->scriptArgs[writeIndex]); // no-op if we are writing to a resized array that was (originally) out of bounds @@ -3639,9 +3641,11 @@ void VM_reset(VMContext* ctx) { ctx->currentEventObjectIndex = -1; ctx->scriptArgs = nullptr; ctx->scriptArgCount = 0; + ctx->scriptArgsOnHeap = false; ctx->currentCodeName = nullptr; ctx->localVars = nullptr; ctx->localVarCount = 0; + ctx->localVarsOnHeap = false; ctx->currentCodeLocalsSlotMap = nullptr; ctx->actionRelativeFlag = false; @@ -3704,7 +3708,16 @@ RValue VM_executeCode(VMContext* ctx, int32_t codeIndex) { setCurrentCodeLocalsSlotMap(ctx); uint32_t localsCount = computeLocalsCount(ctx, code); - RValue* localVars = (RValue *)safeCalloc(localsCount, sizeof(RValue)); + RValue localVarsInline[VM_MAX_STACK_LOCALS]; + RValue* localVars; + if (localsCount <= VM_MAX_STACK_LOCALS) { + localVars = localVarsInline; + memset(localVars, 0, sizeof(RValue) * localsCount); + ctx->localVarsOnHeap = false; + } else { + localVars = (RValue *)safeCalloc(localsCount, sizeof(RValue)); + ctx->localVarsOnHeap = true; + } ctx->localVars = localVars; ctx->localVarCount = localsCount; @@ -3729,9 +3742,10 @@ RValue VM_executeCode(VMContext* ctx, int32_t codeIndex) { repeat(ctx->localVarCount, i) { RValue_free(&ctx->localVars[i]); } - free(ctx->localVars); + if (ctx->localVarsOnHeap) free(ctx->localVars); ctx->localVars = nullptr; ctx->localVarCount = 0; + ctx->localVarsOnHeap = false; // Reset all values in the stack (see issue #137) // Keep in mind that recent GameMaker versions do seem to emit Pop/Popz when exiting loops (example: when using a repeat + return) but older versions DO need it @@ -3754,11 +3768,13 @@ RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t frame.savedBytecodeBase = ctx->bytecodeBase; frame.savedLocals = ctx->localVars; frame.savedLocalsCount = ctx->localVarCount; + frame.savedLocalVarsOnHeap = ctx->localVarsOnHeap; frame.savedCodeName = ctx->currentCodeName; frame.savedSavearefBalance = ctx->savearefBalance; frame.savedCodeLocalsSlotMap = ctx->currentCodeLocalsSlotMap; frame.savedScriptArgs = ctx->scriptArgs; frame.savedScriptArgCount = ctx->scriptArgCount; + frame.savedScriptArgsOnHeap = ctx->scriptArgsOnHeap; frame.savedCurrentCodeIndex = ctx->currentCodeIndex; frame.parent = ctx->callStack; ctx->callStack = &frame; @@ -3776,7 +3792,15 @@ RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t setCurrentCodeLocalsSlotMap(ctx); uint32_t localsCount = computeLocalsCount(ctx, code); - RValue* localVars = (RValue *)safeCalloc(localsCount, sizeof(RValue)); + RValue* localVars; + if (localsCount <= VM_MAX_STACK_LOCALS) { + localVars = frame.inlineLocalVars; + memset(localVars, 0, sizeof(RValue) * localsCount); + ctx->localVarsOnHeap = false; + } else { + localVars = (RValue *)safeCalloc(localsCount, sizeof(RValue)); + ctx->localVarsOnHeap = true; + } ctx->localVars = localVars; ctx->localVarCount = localsCount; @@ -3785,7 +3809,14 @@ RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t // the caller's original args remain valid and owner-tracked by the caller. RValue* scriptArgs = nullptr; if (argCount > 0 && args != nullptr) { - scriptArgs = (RValue *)safeCalloc(argCount, sizeof(RValue)); + if (argCount <= VM_MAX_STACK_ARGS) { + scriptArgs = frame.inlineScriptArgs; + memset(scriptArgs, 0, sizeof(RValue) * argCount); + ctx->scriptArgsOnHeap = false; + } else { + scriptArgs = (RValue *)safeCalloc(argCount, sizeof(RValue)); + ctx->scriptArgsOnHeap = true; + } repeat(argCount, argIdx) { RValue argCopy = RValue_makeIndependent(args[argIdx]); scriptArgs[argIdx] = argCopy; @@ -3821,23 +3852,23 @@ RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t repeat(ctx->localVarCount, i) { RValue_free(&ctx->localVars[i]); } - - free(ctx->localVars); + if (ctx->localVarsOnHeap) free(ctx->localVars); // Free callee script args { - repeat(ctx->scriptArgCount, i) { - RValue_free(&ctx->scriptArgs[i]); - } + repeat(ctx->scriptArgCount, i) { + RValue_free(&ctx->scriptArgs[i]); + } } - - free(ctx->scriptArgs); + if (ctx->scriptArgsOnHeap) free(ctx->scriptArgs); ctx->localVars = saved->savedLocals; ctx->localVarCount = saved->savedLocalsCount; + ctx->localVarsOnHeap = saved->savedLocalVarsOnHeap; ctx->currentCodeLocalsSlotMap = saved->savedCodeLocalsSlotMap; ctx->scriptArgs = saved->savedScriptArgs; ctx->scriptArgCount = saved->savedScriptArgCount; + ctx->scriptArgsOnHeap = saved->savedScriptArgsOnHeap; ctx->currentCodeName = saved->savedCodeName; ctx->currentCodeIndex = saved->savedCurrentCodeIndex; ctx->savearefBalance = saved->savedSavearefBalance; diff --git a/src/vm.h b/src/vm.h index 103239dd7..4a8f60085 100644 --- a/src/vm.h +++ b/src/vm.h @@ -111,9 +111,12 @@ #define BREAK_ISNULLISH (-10) // Pop value, push bool: is the value nullish (undefined / pointer_null)? #define BREAK_PUSHREF (-11) // Push an asset reference (or a script/function reference) encoded in the 32-bit operand -// Max amount of args a function call can have until the args are heap-alloced. +// Max amount of args a function or script call can have until the args are heap-alloced. #define VM_MAX_STACK_ARGS 16 +// Max amount of local variables a code entry can have until the vars are heap-alloced. +#define VM_MAX_STACK_LOCALS 64 + // ===[ Variable Types for V17 Array Access ]=== #define VARTYPE_ARRAYPUSHAF 0x10 // Push array reference (read context) #define VARTYPE_ARRAYPOPAF 0x90 // Push array reference (write context) @@ -133,13 +136,19 @@ typedef struct CallFrame { uint8_t* savedBytecodeBase; RValue* savedLocals; uint32_t savedLocalsCount; + bool savedLocalVarsOnHeap; const char* savedCodeName; int32_t savedSavearefBalance; IntIntHashMap* savedCodeLocalsSlotMap; RValue* savedScriptArgs; int32_t savedScriptArgCount; + bool savedScriptArgsOnHeap; int32_t savedCurrentCodeIndex; struct CallFrame* parent; + + // Stack buffers for the callee's local variables and script args + RValue inlineLocalVars[VM_MAX_STACK_LOCALS]; + RValue inlineScriptArgs[VM_MAX_STACK_ARGS]; } CallFrame; // ===[ EnvFrame - Saved context for with-statement (PushEnv/PopEnv) ]=== @@ -203,6 +212,7 @@ struct VMContext { uint32_t codeEnd; RValue* localVars; uint32_t localVarCount; + bool localVarsOnHeap; struct Instance* globalScopeInstance; // used when GLOB scripts are being executed, and used for the "global" reference struct Instance* currentInstance; struct Instance* otherInstance; // "other" instance for collision events @@ -220,6 +230,7 @@ struct VMContext { EnvFrame* envStack; // Environment stack for with-statements (PushEnv/PopEnv) RValue* scriptArgs; // Arguments passed to current script (nullptr for non-script code) int32_t scriptArgCount; // Number of arguments passed + bool scriptArgsOnHeap; int32_t selfId; int32_t otherId; // Current event context (set by Runner_executeEvent, -1 when not in an event) From 20bc0470bf586dec4ded875f376afc12167a1b43 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 31 Jul 2026 13:25:12 +0100 Subject: [PATCH 2/2] refactor(vm): move stack script-args/locals out of call frame --- src/vm.c | 7 ++++--- src/vm.h | 4 ---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/vm.c b/src/vm.c index a7e74a508..bf35cd11f 100644 --- a/src/vm.c +++ b/src/vm.c @@ -3793,8 +3793,9 @@ RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t uint32_t localsCount = computeLocalsCount(ctx, code); RValue* localVars; + RValue localVarsInline[VM_MAX_STACK_LOCALS]; if (localsCount <= VM_MAX_STACK_LOCALS) { - localVars = frame.inlineLocalVars; + localVars = localVarsInline; memset(localVars, 0, sizeof(RValue) * localsCount); ctx->localVarsOnHeap = false; } else { @@ -3808,10 +3809,10 @@ RValue VM_callCodeIndex(VMContext* ctx, int32_t codeIndex, RValue* args, int32_t // Callee takes an INDEPENDENT reference for strings (strdup) and arrays (incRef) so // the caller's original args remain valid and owner-tracked by the caller. RValue* scriptArgs = nullptr; + RValue scriptArgsInline[VM_MAX_STACK_ARGS]; if (argCount > 0 && args != nullptr) { if (argCount <= VM_MAX_STACK_ARGS) { - scriptArgs = frame.inlineScriptArgs; - memset(scriptArgs, 0, sizeof(RValue) * argCount); + scriptArgs = scriptArgsInline; ctx->scriptArgsOnHeap = false; } else { scriptArgs = (RValue *)safeCalloc(argCount, sizeof(RValue)); diff --git a/src/vm.h b/src/vm.h index 4a8f60085..40e3ca530 100644 --- a/src/vm.h +++ b/src/vm.h @@ -145,10 +145,6 @@ typedef struct CallFrame { bool savedScriptArgsOnHeap; int32_t savedCurrentCodeIndex; struct CallFrame* parent; - - // Stack buffers for the callee's local variables and script args - RValue inlineLocalVars[VM_MAX_STACK_LOCALS]; - RValue inlineScriptArgs[VM_MAX_STACK_ARGS]; } CallFrame; // ===[ EnvFrame - Saved context for with-statement (PushEnv/PopEnv) ]===