Skip to content
Draft
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
8 changes: 7 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2648,8 +2648,14 @@ if(BUILD_UNIT_TESTS)

add_executable(unit_vm_native_module
tests/unit/test_vm_native_module.c
src/qcommon/vm_native_module.c)
tests/stub_qcommon_min.c
src/qcommon/vm_native_module.c
src/qcommon/q_shared.c
src/qcommon/q_math.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()
Expand Down
56 changes: 7 additions & 49 deletions src/qcommon/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1741,64 +1741,22 @@ static void *VM_TryLoadNativeModule( const char *moduleName, char *filename, int
static void * QDECL loadNative( const char *name, vmMainFunc_t *entryPoint, dllSyscall_t systemcalls ) {

char filename[ MAX_QPATH ];
char moduleNames[ VM_MAX_NATIVE_MODULE_LOAD_NAMES ][ MAX_QPATH ];
void *libHandle;
dllEntry_t dllEntry;
void *sym;
void *vmMainAddr;
qboolean isGenericModule = qfalse;
int i;
int moduleNameCount;

// 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 ) {
moduleNameCount = VM_BuildNativeModuleLoadOrder( name, moduleNames, (int)ARRAY_LEN( moduleNames ) );
if ( moduleNameCount > 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 < moduleNameCount; i++ ) {
libHandle = VM_TryLoadNativeModule( moduleNames[i], filename, sizeof( filename ) );
if ( libHandle ) {
goto loadSuccess;
}
Expand Down
57 changes: 57 additions & 0 deletions src/qcommon/vm_native_module.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,63 @@
#include "vm_native_module.h"
#include <stdio.h>

static void VM_CopyNativeModuleName( char *dst, const char *src ) {
(void)snprintf( dst, MAX_QPATH, "%s", src );
}

static qboolean VM_IsGenericNativeModuleName( const char *moduleName ) {
return 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;
}

static int VM_AppendNativeModuleName( char out[][MAX_QPATH], int maxModules, int count, const char *moduleName ) {
if ( count >= maxModules ) {
return count;
}

VM_CopyNativeModuleName( out[count], moduleName );
return count + 1;
}

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 ( !VM_IsGenericNativeModuleName( moduleName ) ) {
return 0;
}

count = VM_AppendNativeModuleName( out, maxModules, count, moduleName );

/* qagame historically probes game.*, and this fork also supports server.*. */
if ( Q_stricmp( moduleName, "qagame" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "game" );
count = VM_AppendNativeModuleName( out, maxModules, count, "server" );
} else if ( Q_stricmp( moduleName, "game" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "server" );
} else if ( Q_stricmp( moduleName, "cgame" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "client" );
} else if ( Q_stricmp( moduleName, "ui" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "frontend" );
} else if ( Q_stricmp( moduleName, "server" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "game" );
} else if ( Q_stricmp( moduleName, "client" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "cgame" );
} else if ( Q_stricmp( moduleName, "frontend" ) == 0 ) {
count = VM_AppendNativeModuleName( out, maxModules, count, "ui" );
}

return count;
}

int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ) {
int count = 0;

Expand Down
11 changes: 11 additions & 0 deletions src/qcommon/vm_native_module.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,15 @@
*/
int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates );

enum {
VM_MAX_NATIVE_MODULE_LOAD_NAMES = 3
};

/*
* Builds logical native VM module names in probe order for the built-in game,
* cgame, and UI aliases. Non-generic module names return zero so loadNative()
* preserves the legacy platform-specific fallback path for custom modules.
*/
int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules );

#endif
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
@@ -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-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`.
81 changes: 81 additions & 0 deletions tests/unit/test_vm_native_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,78 @@ static int test_candidate_limit(void) {
return 0;
}

static int test_load_order_empty_inputs(void) {
char out[VM_MAX_NATIVE_MODULE_LOAD_NAMES][MAX_QPATH];

ASSERT(VM_BuildNativeModuleLoadOrder(NULL, out, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "NULL load-order moduleName");
ASSERT(VM_BuildNativeModuleLoadOrder("", out, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "empty load-order moduleName");
ASSERT(VM_BuildNativeModuleLoadOrder("qagame", NULL, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "NULL load-order output buffer");
ASSERT(VM_BuildNativeModuleLoadOrder("qagame", out, 0) == 0, "zero load-order maxModules");

return 0;
}

static int assert_load_order(const char *moduleName, const char *expected0, const char *expected1, const char *expected2) {
char out[VM_MAX_NATIVE_MODULE_LOAD_NAMES][MAX_QPATH];
int expectedCount = 0;
int count;

if ( expected0 ) {
expectedCount++;
}
if ( expected1 ) {
expectedCount++;
}
if ( expected2 ) {
expectedCount++;
}

count = VM_BuildNativeModuleLoadOrder(moduleName, out, VM_MAX_NATIVE_MODULE_LOAD_NAMES);
ASSERT(count == expectedCount, "load-order count");
if ( expected0 ) {
ASSERT(strcmp(out[0], expected0) == 0, "load-order primary");
}
if ( expected1 ) {
ASSERT(strcmp(out[1], expected1) == 0, "load-order first alias");
}
if ( expected2 ) {
ASSERT(strcmp(out[2], expected2) == 0, "load-order second alias");
}

return 0;
}

static int test_load_order_aliases(void) {
ASSERT(assert_load_order("qagame", "qagame", "game", "server") == 0, "qagame load order");
ASSERT(assert_load_order("game", "game", "server", NULL) == 0, "game load order");
ASSERT(assert_load_order("server", "server", "game", NULL) == 0, "server load order");
ASSERT(assert_load_order("cgame", "cgame", "client", NULL) == 0, "cgame load order");
ASSERT(assert_load_order("client", "client", "cgame", NULL) == 0, "client load order");
ASSERT(assert_load_order("ui", "ui", "frontend", NULL) == 0, "ui load order");
ASSERT(assert_load_order("frontend", "frontend", "ui", NULL) == 0, "frontend load order");

return 0;
}

static int test_load_order_case_and_limits(void) {
char out[VM_MAX_NATIVE_MODULE_LOAD_NAMES][MAX_QPATH];
int count;

count = VM_BuildNativeModuleLoadOrder("QAGAME", out, 2);
ASSERT(count == 2, "limited qagame load-order count");
ASSERT(strcmp(out[0], "QAGAME") == 0, "primary load-order name preserves input case");
ASSERT(strcmp(out[1], "game") == 0, "limited qagame first alias");

count = VM_BuildNativeModuleLoadOrder("FrontEnd", out, VM_MAX_NATIVE_MODULE_LOAD_NAMES);
ASSERT(count == 2, "mixed-case frontend load-order count");
ASSERT(strcmp(out[0], "FrontEnd") == 0, "frontend primary preserves input case");
ASSERT(strcmp(out[1], "ui") == 0, "frontend reverse alias");

ASSERT(VM_BuildNativeModuleLoadOrder("renderer", out, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "custom modules keep legacy fallback path");

return 0;
}

int main(void) {
if (test_empty_inputs() != 0) {
return 1;
Expand All @@ -74,6 +146,15 @@ int main(void) {
if (test_candidate_limit() != 0) {
return 1;
}
if (test_load_order_empty_inputs() != 0) {
return 1;
}
if (test_load_order_aliases() != 0) {
return 1;
}
if (test_load_order_case_and_limits() != 0) {
return 1;
}

printf("PASS: unit_vm_native_module\n");
return 0;
Expand Down
Loading