cnq3/code/qcommon/vm.cpp
arQon 954c80a8b5 merge the new build system
use the "premake" tool from the cnq3tools repo with
"--quake3dir=<q3dir>" to generate a VS sln and gcc makefiles

this changeset also removes the "clever" handling of fullscreen/windowed
changes which didn't actually work and just broke windowed mode
2016-12-29 20:13:59 -08:00

785 lines
18 KiB
C++

/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
This file is part of Quake III Arena source code.
Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
// vm.c -- virtual machine
/*
intermix code and data
symbol table
a dll has one imported function: VM_SystemCall
and one exported function: Perform
*/
#include "vm_local.h"
int vm_debugLevel;
vm_t* currentVM = NULL;
static vm_t* lastVM; // only used by profiling, which is useless anyway
#define MAX_VM 3
static vm_t vmTable[MAX_VM];
static void VM_VmInfo_f( void );
static void VM_VmProfile_f( void );
void VM_Init()
{
Cvar_Get( "vm_cgame", "2", CVAR_ARCHIVE );
Cvar_Get( "vm_game", "2", CVAR_ARCHIVE );
Cvar_Get( "vm_ui", "2", CVAR_ARCHIVE );
Cmd_AddCommand( "vmprofile", VM_VmProfile_f );
Cmd_AddCommand( "vminfo", VM_VmInfo_f );
Com_Memset( vmTable, 0, sizeof( vmTable ) );
}
void VM_Debug( int level )
{
vm_debugLevel = level;
}
// assumes a program counter value
const char* VM_ValueToSymbol( const vm_t* vm, int value )
{
const vmSymbol_t* sym = vm->symbols;
if ( !sym ) {
return "NO SYMBOLS";
}
// find the symbol
while ( sym->next && sym->next->symValue <= value ) {
sym = sym->next;
}
if ( value == sym->symValue ) {
return sym->symName;
}
static char text[MAX_TOKEN_CHARS];
Com_sprintf( text, sizeof( text ), "%s+%i", sym->symName, value - sym->symValue );
return text;
}
// for profiling, find the symbol behind this value
const vmSymbol_t* VM_ValueToFunctionSymbol( const vm_t* vm, int value )
{
static vmSymbol_t nullSym;
const vmSymbol_t* sym = vm->symbols;
if ( !sym ) {
return &nullSym;
}
while ( sym->next && sym->next->symValue <= value ) {
sym = sym->next;
}
return sym;
}
static int ParseHex( const char* text )
{
int value = 0, c;
while ( ( c = *text++ ) != 0 ) {
if ( c >= '0' && c <= '9' ) {
value = value * 16 + c - '0';
continue;
}
if ( c >= 'a' && c <= 'f' ) {
value = value * 16 + 10 + c - 'a';
continue;
}
if ( c >= 'A' && c <= 'F' ) {
value = value * 16 + 10 + c - 'A';
continue;
}
Com_Error( ERR_DROP, "Invalid hex value" );
}
return value;
}
static void VM_LoadSymbols( vm_t* vm )
{
// don't load symbols if not developer
if ( !com_developer->integer ) {
return;
}
int len;
char *mapfile;
char name[MAX_QPATH];
char symbols[MAX_QPATH];
vmSymbol_t **prev, *sym;
int count;
int value;
int chars;
int segment;
int numInstructions;
COM_StripExtension(vm->name, name, sizeof(name));
Com_sprintf( symbols, sizeof( symbols ), "vm/%s.map", name );
len = FS_ReadFile( symbols, (void **)&mapfile );
if ( !mapfile ) {
Com_Printf( "Couldn't load symbol file: %s\n", symbols );
return;
}
numInstructions = vm->instructionPointersLength >> 2;
// parse the symbols
const char* token;
const char* text_p = mapfile;
prev = &vm->symbols;
count = 0;
while ( 1 ) {
token = COM_Parse( &text_p );
if ( !token[0] ) {
break;
}
segment = ParseHex( token );
if ( segment ) {
COM_Parse( &text_p );
COM_Parse( &text_p );
continue; // only load code segment values
}
token = COM_Parse( &text_p );
if ( !token[0] ) {
Com_Printf( "WARNING: incomplete line at end of file\n" );
break;
}
value = ParseHex( token );
token = COM_Parse( &text_p );
if ( !token[0] ) {
Com_Printf( "WARNING: incomplete line at end of file\n" );
break;
}
chars = strlen( token );
sym = (vmSymbol_t*)Hunk_Alloc( sizeof( *sym ) + chars, h_high );
*prev = sym;
prev = &sym->next;
sym->next = NULL;
// convert value from an instruction number to a code offset
if ( value >= 0 && value < numInstructions ) {
value = vm->instructionPointers[value];
}
sym->symValue = value;
Q_strncpyz( sym->symName, token, chars + 1 );
count++;
}
vm->numSymbols = count;
Com_Printf( "%i symbols parsed from %s\n", count, symbols );
FS_FreeFile( mapfile );
}
/*
============
VM_DllSyscall
Dlls will call this directly
rcg010206 The horror; the horror.
The syscall mechanism relies on stack manipulation to get it's args.
This is likely due to C's inability to pass "..." parameters to
a function in one clean chunk. On PowerPC Linux, these parameters
are not necessarily passed on the stack, so while (&arg[0] == arg)
is true, (&arg[1] == 2nd function parameter) is not necessarily
accurate, as arg's value might have been stored to the stack or
other piece of scratch memory to give it a valid address, but the
next parameter might still be sitting in a register.
Quake's syscall system also assumes that the stack grows downward,
and that any needed types can be squeezed, safely, into a signed int.
This hack below copies all needed values for an argument to a
array in memory, so that Quake can get the correct values. This can
also be used on systems where the stack grows upwards, as the
presumably standard and safe stdargs.h macros are used.
As for having enough space in a signed int for your datatypes, well,
it might be better to wait for DOOM 3 before you start porting. :)
The original code, while probably still inherently dangerous, seems
to work well enough for the platforms it already works on. Rather
than add the performance hit for those platforms, the original code
is still in use there.
For speed, we just grab 15 arguments, and don't worry about exactly
how many the syscall actually needs; the extra is thrown away.
============
*/
static intptr_t QDECL VM_DllSyscall( intptr_t arg, ... )
{
#if !id386
// rcg010206 - see commentary above
intptr_t args[16];
int i;
va_list ap;
args[0] = arg;
va_start(ap, arg);
for (i = 1; i < sizeof (args) / sizeof (args[i]); i++)
args[i] = va_arg(ap, intptr_t);
va_end(ap);
return currentVM->systemCall( args );
#else // original id code
return currentVM->systemCall( &arg );
#endif
}
static vmHeader_t* VM_LoadQVM( vm_t* vm, qbool alloc )
{
int length;
int dataLength;
int i;
char filename[MAX_QPATH];
vmHeader_t* header;
Com_sprintf( filename, sizeof(filename), "vm/%s.qvm", vm->name );
length = FS_ReadFile( filename, (void **)&header );
if ( !header )
Com_Error( ERR_FATAL, "Failed to load %s", filename );
Com_Printf( "Loaded %s\n", filename );
if( LittleLong( header->vmMagic ) == VM_MAGIC_VER2 ) {
Com_Printf( "...which has vmMagic VM_MAGIC_VER2\n" );
// byte swap the header
for ( i = 0 ; i < sizeof( vmHeader_t ) / 4 ; i++ ) {
((int *)header)[i] = LittleLong( ((int *)header)[i] );
}
// validate
if ( header->jtrgLength < 0
|| header->bssLength < 0
|| header->dataLength < 0
|| header->litLength < 0
|| header->codeLength <= 0 ) {
VM_Free( vm );
Com_Error( ERR_FATAL, "%s has bad header", filename );
}
} else if( LittleLong( header->vmMagic ) == VM_MAGIC ) {
// byte swap the header
// sizeof( vmHeader_t ) - sizeof( int ) is the 1.32b vm header size
for ( i = 0 ; i < ( sizeof( vmHeader_t ) - sizeof( int ) ) / 4 ; i++ ) {
((int *)header)[i] = LittleLong( ((int *)header)[i] );
}
// validate
if ( header->bssLength < 0
|| header->dataLength < 0
|| header->litLength < 0
|| header->codeLength <= 0 ) {
VM_Free( vm );
Com_Error( ERR_FATAL, "%s has bad header", filename );
}
} else {
VM_Free( vm );
Com_Error( ERR_FATAL, "%s: Invalid header ID", filename );
}
// round up to next power of 2 so all data operations can
// be mask protected
dataLength = header->dataLength + header->litLength + header->bssLength;
for ( i = 0 ; dataLength > ( 1 << i ) ; i++ ) {
}
dataLength = 1 << i;
if( alloc ) {
// allocate zero filled space for initialized and uninitialized data
vm->dataBase = (byte*)Hunk_Alloc( dataLength, h_high );
vm->dataMask = dataLength - 1;
} else {
// clear the data
Com_Memset( vm->dataBase, 0, dataLength );
}
// copy the intialized data
Com_Memcpy( vm->dataBase, (byte *)header + header->dataOffset, header->dataLength + header->litLength );
// byte swap the longs
for ( i = 0 ; i < header->dataLength ; i += 4 ) {
*(int *)(vm->dataBase + i) = LittleLong( *(int *)(vm->dataBase + i ) );
}
if( header->vmMagic == VM_MAGIC_VER2 ) {
vm->numJumpTableTargets = header->jtrgLength >> 2;
Com_Printf( "Loading %d jump table targets\n", vm->numJumpTableTargets );
if( alloc ) {
vm->jumpTableTargets = (byte*)Hunk_Alloc( header->jtrgLength, h_high );
} else {
Com_Memset( vm->jumpTableTargets, 0, header->jtrgLength );
}
Com_Memcpy( vm->jumpTableTargets, (byte *)header + header->dataOffset +
header->dataLength + header->litLength, header->jtrgLength );
// byte swap the longs
for ( i = 0 ; i < header->jtrgLength ; i += 4 ) {
*(int *)(vm->jumpTableTargets + i) = LittleLong( *(int *)(vm->jumpTableTargets + i ) );
}
}
return header;
}
/*
=================
VM_Restart
Reload the data, but leave everything else in place
This allows a server to do a map_restart without changing memory allocation
=================
*/
vm_t *VM_Restart( vm_t *vm ) {
vmHeader_t *header;
// DLL's can't be restarted in place
if ( vm->dllHandle ) {
char name[MAX_QPATH];
intptr_t (*systemCall)( intptr_t *parms );
systemCall = vm->systemCall;
Q_strncpyz( name, vm->name, sizeof( name ) );
VM_Free( vm );
vm = VM_Create( name, systemCall, VMI_NATIVE );
return vm;
}
// load the image
Com_Printf( "VM_Restart()\n" );
if( !( header = VM_LoadQVM( vm, qfalse ) ) ) {
Com_Error( ERR_DROP, "VM_Restart failed.\n" );
return NULL;
}
// free the original file
FS_FreeFile( header );
return vm;
}
/*
================
VM_Create
If image ends in .qvm it will be interpreted, otherwise
it will attempt to load as a system dll
================
*/
#define STACK_SIZE 0x20000
vm_t* VM_Create( const char *module, intptr_t (*systemCalls)(intptr_t *), vmInterpret_t interpret )
{
vm_t *vm;
vmHeader_t *header;
int i, remaining;
if ( !module || !module[0] || !systemCalls ) {
Com_Error( ERR_FATAL, "VM_Create: bad parms" );
}
remaining = Hunk_MemoryRemaining();
// see if we already have the VM
for ( i = 0 ; i < MAX_VM ; i++ ) {
if (!Q_stricmp(vmTable[i].name, module)) {
vm = &vmTable[i];
return vm;
}
}
// find a free vm
for ( i = 0 ; i < MAX_VM ; i++ ) {
if ( !vmTable[i].name[0] ) {
break;
}
}
if ( i == MAX_VM ) {
Com_Error( ERR_FATAL, "VM_Create: no free vm_t" );
}
vm = &vmTable[i];
Q_strncpyz( vm->name, module, sizeof( vm->name ) );
vm->systemCall = systemCalls;
if ( interpret == VMI_NATIVE ) {
// try to load as a system dll
Com_DPrintf( "Loading dll file %s\n", vm->name );
vm->dllHandle = Sys_LoadDll( module, &vm->entryPoint, VM_DllSyscall );
if ( vm->dllHandle )
return vm;
Com_DPrintf( "Failed to load dll, looking for qvm.\n" );
}
// load the image
if( !( header = VM_LoadQVM( vm, qtrue ) ) ) {
return NULL;
}
// allocate space for the jump targets, which will be filled in by the compile/prep functions
vm->instructionPointersLength = header->instructionCount * 4;
vm->instructionPointers = (int*)Hunk_Alloc( vm->instructionPointersLength, h_high );
// copy or compile the instructions
vm->codeLength = header->codeLength;
#if defined(NO_VM_COMPILED)
Com_Printf("Architecture doesn't have a bytecode compiler, using interpreter\n");
vm->compiled = qfalse;
VM_PrepareInterpreter( vm, header );
#else
vm->compiled = qtrue;
VM_Compile( vm, header );
// VM_Compile will have reset vm->compiled if compilation failed
if (!vm->compiled)
Com_Error( ERR_FATAL, "ERROR: QVM compilation failed\n" );
#endif
// free the original file
FS_FreeFile( header );
// load the map file
VM_LoadSymbols( vm );
// the stack is implicitly at the end of the image
vm->programStack = vm->dataMask + 1;
vm->stackBottom = vm->programStack - STACK_SIZE;
Com_DPrintf("%s loaded in %d bytes on the hunk\n", module, remaining - Hunk_MemoryRemaining());
return vm;
}
/*
==============
VM_Free
==============
*/
void VM_Free( vm_t *vm ) {
if(vm->destroy)
vm->destroy(vm);
if ( vm->dllHandle ) {
Sys_UnloadDll( vm->dllHandle );
Com_Memset( vm, 0, sizeof( *vm ) );
}
#if 0 // now automatically freed by hunk
if ( vm->codeBase ) {
Z_Free( vm->codeBase );
}
if ( vm->dataBase ) {
Z_Free( vm->dataBase );
}
if ( vm->instructionPointers ) {
Z_Free( vm->instructionPointers );
}
#endif
Com_Memset( vm, 0, sizeof( *vm ) );
currentVM = NULL;
lastVM = NULL;
}
void VM_Clear(void) {
int i;
for (i=0;i<MAX_VM; i++) {
if ( vmTable[i].dllHandle ) {
Sys_UnloadDll( vmTable[i].dllHandle );
}
Com_Memset( &vmTable[i], 0, sizeof( vm_t ) );
}
currentVM = NULL;
lastVM = NULL;
}
intptr_t VM_ArgPtr( intptr_t intValue )
{
if (!intValue || !currentVM)
return 0;
if ( currentVM->entryPoint ) {
return (intptr_t)(currentVM->dataBase + intValue);
}
else {
return (intptr_t)(currentVM->dataBase + (intValue & currentVM->dataMask));
}
}
intptr_t VM_ExplicitArgPtr( const vm_t* vm, intptr_t intValue )
{
if (!intValue)
return 0;
// bk010124 - currentVM is missing on reconnect here as well?
if (!currentVM)
return 0;
if ( vm->entryPoint ) {
return (intptr_t)(vm->dataBase + intValue);
}
else {
return (intptr_t)(vm->dataBase + (intValue & vm->dataMask));
}
}
/*
==============
VM_Call
Upon a system call, the stack will look like:
sp+32 parm1
sp+28 parm0
sp+24 return value
sp+20 return address
sp+16 local1
sp+14 local0
sp+12 arg1
sp+8 arg0
sp+4 return stack
sp return address
An interpreted function will immediately execute
an OP_ENTER instruction, which will subtract space for
locals from sp
==============
*/
#define MAX_STACK 256
#define STACK_MASK (MAX_STACK-1)
intptr_t QDECL VM_Call( vm_t *vm, int callnum, ... ) {
vm_t *oldVM;
intptr_t r;
int i;
if ( !vm ) {
Com_Error( ERR_FATAL, "VM_Call with NULL vm" );
}
oldVM = currentVM;
currentVM = vm;
lastVM = vm;
if ( vm_debugLevel ) {
Com_Printf( "VM_Call( %ld )\n", callnum );
}
// if we have a dll loaded, call it directly
if ( vm->entryPoint ) {
//rcg010207 - see dissertation at top of VM_DllSyscall() in this file.
int args[10];
va_list ap;
va_start(ap, callnum);
for (i = 0; i < sizeof (args) / sizeof (args[i]); i++) {
args[i] = va_arg(ap, int);
}
va_end(ap);
r = vm->entryPoint( callnum, args[0], args[1], args[2], args[3],
args[4], args[5], args[6], args[7],
args[8], args[9]);
} else {
#if id386 // i386 calling convention doesn't need conversion
#if !defined(NO_VM_COMPILED)
r = VM_CallCompiled( vm, (int*)&callnum );
#else
r = VM_CallInterpreted( vm, (int*)&callnum );
#endif
#else
struct {
int callnum;
int args[10];
} a;
va_list ap;
a.callnum = callnum;
va_start(ap, callnum);
for (i = 0; i < sizeof (a.args) / sizeof (a.args[0]); i++) {
a.args[i] = va_arg(ap, int);
}
va_end(ap);
#if !defined(NO_VM_COMPILED)
r = VM_CallCompiled( vm, &a.callnum );
#else
r = VM_CallInterpreted( vm, &a.callnum );
#endif
#endif
}
if ( oldVM != NULL ) // bk001220 - assert(currentVM!=NULL) for oldVM==NULL
currentVM = oldVM;
return r;
}
///////////////////////////////////////////////////////////////
static int QDECL VM_ProfileSort( const void *a, const void *b )
{
const vmSymbol_t *sa, *sb;
sa = *(vmSymbol_t **)a;
sb = *(vmSymbol_t **)b;
if ( sa->profileCount < sb->profileCount ) {
return -1;
}
if ( sa->profileCount > sb->profileCount ) {
return 1;
}
return 0;
}
static void VM_VmProfile_f( void )
{
vmSymbol_t **sorted, *sym;
int i;
double total;
if ( !lastVM ) {
return;
}
const vm_t* vm = lastVM;
if ( !vm->numSymbols ) {
return;
}
sorted = (vmSymbol_t**)Z_Malloc( vm->numSymbols * sizeof( *sorted ) );
sorted[0] = vm->symbols;
total = sorted[0]->profileCount;
for ( i = 1 ; i < vm->numSymbols ; i++ ) {
sorted[i] = sorted[i-1]->next;
total += sorted[i]->profileCount;
}
qsort( sorted, vm->numSymbols, sizeof( *sorted ), VM_ProfileSort );
for ( i = 0 ; i < vm->numSymbols ; i++ ) {
int perc;
sym = sorted[i];
perc = 100 * (float) sym->profileCount / total;
Com_Printf( "%2i%% %9i %s\n", perc, sym->profileCount, sym->symName );
sym->profileCount = 0;
}
Com_Printf(" %9.0f total\n", total );
Z_Free( sorted );
}
static void VM_VmInfo_f( void )
{
Com_Printf( "Registered virtual machines:\n" );
for (int i = 0; i < MAX_VM; ++i) {
const vm_t* vm = &vmTable[i];
if ( !vm->name[0] ) {
break;
}
Com_Printf( "%s : ", vm->name );
if ( vm->dllHandle ) {
Com_Printf( "native\n" );
continue;
}
if ( vm->compiled ) {
Com_Printf( "compiled on load\n" );
} else {
Com_Printf( "interpreted\n" );
}
Com_Printf( " code length : %7i\n", vm->codeLength );
Com_Printf( " table length: %7i\n", vm->instructionPointersLength );
Com_Printf( " data length : %7i\n", vm->dataMask + 1 );
}
}
///////////////////////////////////////////////////////////////
/*
===============
VM_LogSyscalls
Insert calls to this while debugging the vm compiler
===============
*/
void VM_LogSyscalls( int *args ) {
static int callnum;
static FILE *f;
if ( !f ) {
f = fopen("syscalls.log", "w" );
}
callnum++;
fprintf(f, "%i: %p (%i) = %i %i %i %i\n", callnum, (void*)(args - (int *)currentVM->dataBase),
args[0], args[1], args[2], args[3], args[4] );
}