diff --git a/CMakeLists.txt b/CMakeLists.txt index 38f9da8b00..938a285c96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2648,8 +2648,14 @@ if(BUILD_UNIT_TESTS) add_executable(unit_vm_native_module tests/unit/test_vm_native_module.c + tests/stub_qcommon_min.c + src/qcommon/q_shared.c + src/qcommon/q_math.c src/qcommon/vm_native_module.c) target_include_directories(unit_vm_native_module PRIVATE ${CMAKE_SOURCE_DIR}/src) + if(NOT MSVC) + target_link_libraries(unit_vm_native_module PRIVATE m) + endif() add_test(NAME unit_vm_native_module COMMAND unit_vm_native_module) set_tests_properties(unit_vm_native_module PROPERTIES LABELS "unit;validation") endif() diff --git a/src/qcommon/vm.c b/src/qcommon/vm.c index 64daf86413..4e0738a7b3 100644 --- a/src/qcommon/vm.c +++ b/src/qcommon/vm.c @@ -1746,59 +1746,15 @@ static void * QDECL loadNative( const char *name, vmMainFunc_t *entryPoint, dllS void *sym; void *vmMainAddr; qboolean isGenericModule = qfalse; + char moduleNames[3][MAX_QPATH]; + int moduleCount; + int i; - // For ui, game, cgame, qagame and custom aliases, try generic names first. - if ( Q_stricmp( name, "ui" ) == 0 || Q_stricmp( name, "game" ) == 0 || Q_stricmp( name, "cgame" ) == 0 || - Q_stricmp( name, "qagame" ) == 0 || Q_stricmp( name, "frontend" ) == 0 || - Q_stricmp( name, "client" ) == 0 || Q_stricmp( name, "server" ) == 0 ) { + moduleCount = VM_BuildNativeModuleLoadOrder( name, moduleNames, (int)ARRAY_LEN( moduleNames ) ); + if ( moduleCount > 0 ) { isGenericModule = qtrue; - - libHandle = VM_TryLoadNativeModule( name, filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - - // qagame has historically been loaded from game.* as an alias. - if ( Q_stricmp( name, "qagame" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "game", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - - // Support renamed native modules for this project. - if ( Q_stricmp( name, "qagame" ) == 0 || Q_stricmp( name, "game" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "server", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "cgame" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "client", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "ui" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "frontend", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "server" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "game", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "client" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "cgame", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "frontend" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "ui", filename, sizeof( filename ) ); + for ( i = 0; i < moduleCount; i++ ) { + libHandle = VM_TryLoadNativeModule( moduleNames[i], filename, sizeof( filename ) ); if ( libHandle ) { goto loadSuccess; } @@ -2051,7 +2007,6 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... ) { //vm_t *oldVM; intptr_t r; - int i; if ( !vm ) { Com_Error( ERR_FATAL, "VM_Call with NULL vm" ); @@ -2077,19 +2032,12 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... ) // if we have a dll loaded, call it directly if ( vm->entryPoint ) { - /* Pass three arg slots: native vmMain only receives nargs from varargs; zero the rest - * so e.g. UI_GETAPIVERSION (nargs=0) does not read stack garbage. */ - int32_t args[MAX_VMMAIN_CALL_ARGS-1]; - Com_Memset( args, 0, sizeof( args ) ); va_list ap; + _Static_assert( VM_NATIVE_MODULE_ARG_COUNT == MAX_VMMAIN_CALL_ARGS - 1, + "native vmMain helper must pass every native arg slot" ); va_start( ap, callnum ); - for ( i = 0; i < nargs; i++ ) { - args[i] = va_arg( ap, int32_t ); - } + r = VM_CallNativeModuleEntryPoint( vm->entryPoint, nargs, callnum, ap ); va_end( ap ); - - // add more arguments if you're changed MAX_VMMAIN_CALL_ARGS: - r = vm->entryPoint( callnum, args[0], args[1], args[2] ); } else { #if id386 && !defined __clang__ // calling convention doesn't need conversion in some cases #ifndef NO_VM_COMPILED @@ -2100,6 +2048,7 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... ) r = VM_CallInterpreted2( vm, nargs+1, (int32_t*)&callnum ); #else int32_t args[MAX_VMMAIN_CALL_ARGS]; + int i; va_list ap; args[0] = callnum; diff --git a/src/qcommon/vm_native_module.c b/src/qcommon/vm_native_module.c index be2f9d7b78..0260e96012 100644 --- a/src/qcommon/vm_native_module.c +++ b/src/qcommon/vm_native_module.c @@ -1,6 +1,15 @@ #include "vm_native_module.h" #include +static void VM_AddNativeModuleName( char out[][MAX_QPATH], int maxModules, int *count, const char *moduleName ) { + if ( *count >= maxModules ) { + return; + } + + (void)snprintf( out[*count], MAX_QPATH, "%s", moduleName ); + (*count)++; +} + int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ) { int count = 0; @@ -24,3 +33,56 @@ int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH return count; } + +int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules ) { + int count = 0; + + if ( !moduleName || !moduleName[0] || !out || maxModules <= 0 ) { + return 0; + } + + if ( Q_stricmp( moduleName, "ui" ) != 0 && Q_stricmp( moduleName, "game" ) != 0 && + Q_stricmp( moduleName, "cgame" ) != 0 && Q_stricmp( moduleName, "qagame" ) != 0 && + Q_stricmp( moduleName, "frontend" ) != 0 && Q_stricmp( moduleName, "client" ) != 0 && + Q_stricmp( moduleName, "server" ) != 0 ) { + return 0; + } + + VM_AddNativeModuleName( out, maxModules, &count, moduleName ); + + if ( Q_stricmp( moduleName, "qagame" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "game" ); + } + if ( Q_stricmp( moduleName, "qagame" ) == 0 || Q_stricmp( moduleName, "game" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "server" ); + } + if ( Q_stricmp( moduleName, "cgame" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "client" ); + } + if ( Q_stricmp( moduleName, "ui" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "frontend" ); + } + if ( Q_stricmp( moduleName, "server" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "game" ); + } + if ( Q_stricmp( moduleName, "client" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "cgame" ); + } + if ( Q_stricmp( moduleName, "frontend" ) == 0 ) { + VM_AddNativeModuleName( out, maxModules, &count, "ui" ); + } + + return count; +} + +intptr_t VM_CallNativeModuleEntryPoint( vmNativeModuleEntryPoint_t entryPoint, int nargs, int callnum, va_list ap ) { + int32_t args[VM_NATIVE_MODULE_ARG_COUNT]; + int i; + + Com_Memset( args, 0, sizeof( args ) ); + for ( i = 0; i < nargs; i++ ) { + args[i] = va_arg( ap, int32_t ); + } + + return entryPoint( callnum, args[0], args[1], args[2] ); +} diff --git a/src/qcommon/vm_native_module.h b/src/qcommon/vm_native_module.h index e8bd630f99..ad35a26c6a 100644 --- a/src/qcommon/vm_native_module.h +++ b/src/qcommon/vm_native_module.h @@ -3,6 +3,10 @@ #include "q_shared.h" +#define VM_NATIVE_MODULE_ARG_COUNT 3 + +typedef intptr_t (QDECL *vmNativeModuleEntryPoint_t)( int command, int arg0, int arg1, int arg2 ); + /* * Builds native VM module filename candidates in priority order: * 1) module.so @@ -11,4 +15,12 @@ */ int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ); +/* + * Builds logical native VM module names in the order loadNative() should probe + * them before falling back to the legacy platform-specific filename. + */ +int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules ); + +intptr_t VM_CallNativeModuleEntryPoint( vmNativeModuleEntryPoint_t entryPoint, int nargs, int callnum, va_list ap ); + #endif diff --git a/tests/README.md b/tests/README.md index f155d19aa4..7e837883d9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,5 @@ # Tests -- **Unit** (`BUILD_UNIT_TESTS=ON`): `unit_macros`, `unit_qmath`, `unit_surfaceflags`, `unit_qhelpers`, `unit_crc`, `unit_pathutil`, `unit_msg`, `unit_info`, `unit_cm_bounds`, `unit_parse`, `unit_endian` (CRC, COM path, `Info_*`, `COM_Parse*`, and endian tests use the same minimal `stub_qcommon_min.c` + `q_shared.c` + `q_math.c` link as `unit_qhelpers`; `unit_msg` links `msg.c` + `huffman_static.c` with `stub_qcommon_min.c`, `stub_msg_cvar.c`, and `-DDEDICATED`; `unit_cm_bounds` links `cm_bounds.c` + `q_math.c` only) — run `ctest -R unit_` or `./unit_*` from the build directory. +- **Unit** (`BUILD_UNIT_TESTS=ON`): `unit_macros`, `unit_qmath`, `unit_surfaceflags`, `unit_qhelpers`, `unit_crc`, `unit_pathutil`, `unit_msg`, `unit_info`, `unit_cm_bounds`, `unit_parse`, `unit_endian`, `unit_vm_native_module` (CRC, COM path, `Info_*`, `COM_Parse*`, endian, and native VM module tests use the same minimal `stub_qcommon_min.c` + `q_shared.c` + `q_math.c` link as `unit_qhelpers`; `unit_msg` links `msg.c` + `huffman_static.c` with `stub_qcommon_min.c`, `stub_msg_cvar.c`, and `-DDEDICATED`; `unit_cm_bounds` links `cm_bounds.c` + `q_math.c` only) — run `ctest -R unit_` or `./unit_*` from the build directory. - **Script regression tests**: `test_botlib_bounded_strings` (botlib string invariants). Run with `ctest -R test_botlib_bounded_strings` from the build directory. - **Validation**: `smoke_test`, `renderer_regression_check`, `check_artifacts`, `test_run_vulkan_script`, `test_compile_engine_lto`, `test_demo_game_pk3`, `test_vk_vegetation_dispatch_order`, `test_vulkan_mesh_shader_opt_in`, `test_vulkan_runtime_regressions`, `test_botlib_chat_message_bounds`, `test_vulkan_renderer_guards`, `test_vulkan_regression_source_guards`, `test_gltf_opengl_regressions`, `test_botlib_bounded_strings` — see `scripts/`, `tests/scripts/`, and `docs/RENDERER_CONFIDENCE.md`. diff --git a/tests/unit/test_vm_native_module.c b/tests/unit/test_vm_native_module.c index 51ee8d9035..6a98368b4f 100644 --- a/tests/unit/test_vm_native_module.c +++ b/tests/unit/test_vm_native_module.c @@ -64,6 +64,120 @@ static int test_candidate_limit(void) { return 0; } +static int assert_load_order(const char *moduleName, int maxModules, int expectedCount, const char **expected) { + char out[3][MAX_QPATH]; + int count; + int i; + + count = VM_BuildNativeModuleLoadOrder(moduleName, out, maxModules); + ASSERT(count == expectedCount, "load-order count"); + for (i = 0; i < count; i++) { + ASSERT(strcmp(out[i], expected[i]) == 0, "load-order entry"); + } + + return 0; +} + +static int test_load_order_aliases(void) { + const char *qagame[] = { "qagame", "game", "server" }; + const char *game[] = { "game", "server" }; + const char *cgame[] = { "cgame", "client" }; + const char *ui[] = { "ui", "frontend" }; + const char *server[] = { "server", "game" }; + const char *client[] = { "client", "cgame" }; + const char *frontend[] = { "frontend", "ui" }; + const char *mixedCase[] = { "QAGAME", "game", "server" }; + + if (assert_load_order("qagame", 3, 3, qagame) != 0) { + return 1; + } + if (assert_load_order("game", 3, 2, game) != 0) { + return 1; + } + if (assert_load_order("cgame", 3, 2, cgame) != 0) { + return 1; + } + if (assert_load_order("ui", 3, 2, ui) != 0) { + return 1; + } + if (assert_load_order("server", 3, 2, server) != 0) { + return 1; + } + if (assert_load_order("client", 3, 2, client) != 0) { + return 1; + } + if (assert_load_order("frontend", 3, 2, frontend) != 0) { + return 1; + } + if (assert_load_order("QAGAME", 3, 3, mixedCase) != 0) { + return 1; + } + + return 0; +} + +static int test_load_order_limits_and_custom_modules(void) { + const char *limited[] = { "qagame", "game" }; + char out[3][MAX_QPATH]; + + if (assert_load_order("qagame", 2, 2, limited) != 0) { + return 1; + } + + ASSERT(VM_BuildNativeModuleLoadOrder(NULL, out, 3) == 0, "NULL load-order input"); + ASSERT(VM_BuildNativeModuleLoadOrder("", out, 3) == 0, "empty load-order input"); + ASSERT(VM_BuildNativeModuleLoadOrder("custommod", out, 3) == 0, "custom module should use legacy platform-specific fallback only"); + ASSERT(VM_BuildNativeModuleLoadOrder("qagame", NULL, 3) == 0, "NULL load-order output"); + ASSERT(VM_BuildNativeModuleLoadOrder("qagame", out, 0) == 0, "zero load-order capacity"); + + return 0; +} + +static int capturedCommand; +static int capturedArg0; +static int capturedArg1; +static int capturedArg2; + +static intptr_t QDECL capture_entry_point(int command, int arg0, int arg1, int arg2) { + capturedCommand = command; + capturedArg0 = arg0; + capturedArg1 = arg1; + capturedArg2 = arg2; + return 1234; +} + +static intptr_t call_native_entry_point(int nargs, int callnum, ...) { + intptr_t result; + va_list ap; + + va_start(ap, callnum); + result = VM_CallNativeModuleEntryPoint(capture_entry_point, nargs, callnum, ap); + va_end(ap); + return result; +} + +static int test_native_call_args_zero_fill(void) { + ASSERT(call_native_entry_point(0, 77) == 1234, "native entry return value"); + ASSERT(capturedCommand == 77, "zero-arg callnum"); + ASSERT(capturedArg0 == 0, "zero-arg arg0"); + ASSERT(capturedArg1 == 0, "zero-arg arg1"); + ASSERT(capturedArg2 == 0, "zero-arg arg2"); + + ASSERT(call_native_entry_point(1, 88, 111) == 1234, "one-arg native entry return value"); + ASSERT(capturedCommand == 88, "one-arg callnum"); + ASSERT(capturedArg0 == 111, "one-arg arg0"); + ASSERT(capturedArg1 == 0, "one-arg arg1 zero-filled"); + ASSERT(capturedArg2 == 0, "one-arg arg2 zero-filled"); + + ASSERT(call_native_entry_point(3, 99, 11, 22, 33) == 1234, "three-arg native entry return value"); + ASSERT(capturedCommand == 99, "three-arg callnum"); + ASSERT(capturedArg0 == 11, "three-arg arg0"); + ASSERT(capturedArg1 == 22, "three-arg arg1"); + ASSERT(capturedArg2 == 33, "three-arg arg2"); + + return 0; +} + int main(void) { if (test_empty_inputs() != 0) { return 1; @@ -74,6 +188,15 @@ int main(void) { if (test_candidate_limit() != 0) { return 1; } + if (test_load_order_aliases() != 0) { + return 1; + } + if (test_load_order_limits_and_custom_modules() != 0) { + return 1; + } + if (test_native_call_args_zero_fill() != 0) { + return 1; + } printf("PASS: unit_vm_native_module\n"); return 0;