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
6 changes: 6 additions & 0 deletions 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
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()
Expand Down
73 changes: 11 additions & 62 deletions src/qcommon/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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" );
Expand All @@ -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
Expand All @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions src/qcommon/vm_native_module.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#include "vm_native_module.h"
#include <stdio.h>

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;

Expand All @@ -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] );
}
12 changes: 12 additions & 0 deletions src/qcommon/vm_native_module.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
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 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`.
123 changes: 123 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,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;
Expand All @@ -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;
Expand Down
Loading