diff --git a/neo/engine/doomdll.vcxproj b/neo/engine/doomdll.vcxproj index ad5d85c6..84ca1818 100644 --- a/neo/engine/doomdll.vcxproj +++ b/neo/engine/doomdll.vcxproj @@ -409,6 +409,8 @@ + + @@ -483,6 +485,7 @@ + @@ -1616,6 +1619,7 @@ + @@ -2857,6 +2861,7 @@ + diff --git a/neo/engine/doomdll.vcxproj.filters b/neo/engine/doomdll.vcxproj.filters index fa236aac..1f227651 100644 --- a/neo/engine/doomdll.vcxproj.filters +++ b/neo/engine/doomdll.vcxproj.filters @@ -13,6 +13,9 @@ {19b363f5-76a7-4256-b7f0-7a6ec012d09d} + + {a48802f5-5084-4d4a-bd3d-7161c1885b64} + {2b6b512d-2388-41b9-8880-402e156ec20b} @@ -255,6 +258,12 @@ Renderer + + Minigame + + + Minigame + Sound @@ -519,6 +528,9 @@ Ui + + Ui + Ui @@ -812,6 +824,9 @@ Renderer + + Minigame + Sound @@ -1571,6 +1586,9 @@ Ui + + Ui + Ui diff --git a/neo/engine/minigame/MinigameHost.cpp b/neo/engine/minigame/MinigameHost.cpp new file mode 100644 index 00000000..6d139c92 --- /dev/null +++ b/neo/engine/minigame/MinigameHost.cpp @@ -0,0 +1,315 @@ +/* +=========================================================================== + +IceTech GPL Source Code +Copyright (C) 2026 Justin Marshall + +This file is part of the IceTech GPL Source Code. + +=========================================================================== +*/ + +#include "precompiled.h" +#pragma hdrstop + +#include "MinigameHost.h" +#include "../renderer/RenderSystem.h" + +idMinigameInstance::idMinigameInstance() { + moduleHandle = 0; + memset( &exports, 0, sizeof( exports ) ); + memset( &imports, 0, sizeof( imports ) ); + instance = NULL; + memset( &threadInfo, 0, sizeof( threadInfo ) ); + InitializeCriticalSection( &criticalSection ); + threadStarted = false; + shutdownRequested = false; + active = false; + activeSent = false; + failed = false; + frameReady = false; + framebuffer = NULL; + framebufferWidth = 0; + framebufferHeight = 0; + framebufferBytes = 0; +} + +idMinigameInstance::~idMinigameInstance() { + Shutdown(); + DeleteCriticalSection( &criticalSection ); +} + +void idMinigameInstance::Lock() { + EnterCriticalSection( &criticalSection ); +} + +void idMinigameInstance::Unlock() { + LeaveCriticalSection( &criticalSection ); +} + +void idMinigameInstance::PrintCallback( void *userData, const char *text ) { + if ( text && text[0] ) { + common->Printf( "minigame: %s", text ); + } +} + +void idMinigameInstance::SubmitAudioCallback( void *userData, const iceMinigameAudioBuffer_t *audio ) { + idMinigameInstance *self = reinterpret_cast( userData ); + if ( !self || !audio || !audio->samples || audio->frames <= 0 ) { + return; + } + + // The ABI already carries raw PCM across the DLL boundary. The engine mixer + // bridge will consume this callback once the minigame sound emitter lands. +} + +bool idMinigameInstance::Start( const char *module, const char *image, const char *cmdLine ) { + Shutdown(); + + if ( !module || !module[0] || !image || !image[0] ) { + failed = true; + return false; + } + + moduleName = module; + imageName = image; + commandLine = cmdLine ? cmdLine : ""; + failed = false; + shutdownRequested = false; + + idStr relativePath; + if ( moduleName.Find( "/" ) >= 0 || moduleName.Find( "\\" ) >= 0 ) { + relativePath = moduleName; + } else { + relativePath = "minigames/"; + relativePath += moduleName; + } + if ( relativePath.Find( "." ) < 0 ) { + relativePath += ".minigame"; + } + + resolvedPath = fileSystem->RelativePathToOSPath( relativePath.c_str(), "fs_basepath" ); + moduleHandle = Sys_DLL_Load( resolvedPath.c_str() ); + if ( !moduleHandle ) { + common->Warning( "Could not load minigame '%s' from '%s'", moduleName.c_str(), resolvedPath.c_str() ); + failed = true; + return false; + } + + iceMinigameGetAPI_t GetMinigameAPI = (iceMinigameGetAPI_t)Sys_DLL_GetProcAddress( moduleHandle, ICE_MINIGAME_ENTRY_POINT ); + if ( !GetMinigameAPI ) { + common->Warning( "Minigame '%s' does not export %s", moduleName.c_str(), ICE_MINIGAME_ENTRY_POINT ); + Shutdown(); + failed = true; + return false; + } + + imports.version = ICE_MINIGAME_API_VERSION; + imports.userData = this; + importBasePath = fileSystem->RelativePathToOSPath( "", "fs_basepath" ); + importSavePath = fileSystem->RelativePathToOSPath( "", "fs_savepath" ); + imports.basePath = importBasePath.c_str(); + imports.savePath = importSavePath.c_str(); + imports.Print = PrintCallback; + imports.SubmitAudio = SubmitAudioCallback; + + if ( !GetMinigameAPI( &imports, &exports ) || exports.version != ICE_MINIGAME_API_VERSION || + !exports.CreateInstance || !exports.DestroyInstance || !exports.RunFrame || !exports.GetFramebuffer ) { + common->Warning( "Minigame '%s' has an incompatible API", moduleName.c_str() ); + Shutdown(); + failed = true; + return false; + } + + if ( !exports.CreateInstance( &imports, commandLine.c_str(), &instance ) || !instance ) { + common->Warning( "Minigame '%s' failed to create an instance", moduleName.c_str() ); + Shutdown(); + failed = true; + return false; + } + + if ( renderSystem && exports.framebufferWidth > 0 && exports.framebufferHeight > 0 ) { + renderSystem->CreateScratchImage( imageName.c_str(), exports.framebufferWidth, exports.framebufferHeight ); + } + + Sys_CreateThread( ThreadProc, this, THREAD_NORMAL, threadInfo, "minigame", g_threads, &g_thread_count ); + threadStarted = true; + return true; +} + +void idMinigameInstance::Shutdown() { + shutdownRequested = true; + if ( threadStarted && threadInfo.threadHandle ) { + Sys_DestroyThread( threadInfo ); + } + threadStarted = false; + shutdownRequested = false; + + if ( exports.DestroyInstance && instance ) { + exports.DestroyInstance( instance ); + } + instance = NULL; + + if ( moduleHandle ) { + Sys_DLL_Unload( moduleHandle ); + } + moduleHandle = 0; + memset( &exports, 0, sizeof( exports ) ); + + Lock(); + pendingEvents.Clear(); + ClearFramebuffer(); + frameReady = false; + active = false; + activeSent = false; + Unlock(); +} + +void idMinigameInstance::SetActive( bool setActive ) { + Lock(); + active = setActive; + Unlock(); +} + +void idMinigameInstance::QueueEvent( const sysEvent_t *event, int time ) { + if ( !event || !instance ) { + return; + } + + iceMinigameInputEvent_t minigameEvent; + minigameEvent.type = ICE_MINIGAME_EVENT_NONE; + minigameEvent.value = event->evValue; + minigameEvent.value2 = event->evValue2; + minigameEvent.time = time; + + switch ( event->evType ) { + case SE_KEY: + minigameEvent.type = ICE_MINIGAME_EVENT_KEY; + break; + case SE_CHAR: + minigameEvent.type = ICE_MINIGAME_EVENT_CHAR; + break; + case SE_MOUSE: + minigameEvent.type = ICE_MINIGAME_EVENT_MOUSE; + break; + default: + return; + } + + Lock(); + if ( pendingEvents.Num() < 256 ) { + pendingEvents.Append( minigameEvent ); + } + Unlock(); +} + +bool idMinigameInstance::UploadFramebuffer() { + if ( !renderSystem || !instance ) { + return false; + } + + Lock(); + if ( !frameReady || !framebuffer || framebufferWidth <= 0 || framebufferHeight <= 0 ) { + Unlock(); + return false; + } + const bool uploaded = renderSystem->UploadImage( imageName.c_str(), framebuffer, framebufferWidth, framebufferHeight ); + frameReady = false; + Unlock(); + return uploaded; +} + +unsigned int idMinigameInstance::ThreadProc( void *parm ) { + idMinigameInstance *self = reinterpret_cast( parm ); + return self ? self->RunThread() : 0; +} + +unsigned int idMinigameInstance::RunThread() { + int lastTime = Sys_Milliseconds(); + + while ( !shutdownRequested ) { + iceMinigameInputEvent_t events[256]; + int numEvents = 0; + bool nowActive; + bool sendActive; + + Lock(); + nowActive = active; + sendActive = ( activeSent != active ); + activeSent = active; + numEvents = pendingEvents.Num(); + if ( numEvents > 256 ) { + numEvents = 256; + } + for ( int i = 0; i < numEvents; i++ ) { + events[i] = pendingEvents[i]; + } + pendingEvents.Clear(); + Unlock(); + + if ( sendActive && exports.SetActive ) { + exports.SetActive( instance, nowActive ? 1 : 0 ); + } + + for ( int i = 0; i < numEvents; i++ ) { + if ( exports.QueueEvent ) { + exports.QueueEvent( instance, &events[i] ); + } + } + + const int now = Sys_Milliseconds(); + const int elapsed = Max( 1, now - lastTime ); + lastTime = now; + + if ( nowActive || numEvents > 0 ) { + exports.RunFrame( instance, elapsed ); + + iceMinigameFramebuffer_t framebufferInfo; + memset( &framebufferInfo, 0, sizeof( framebufferInfo ) ); + if ( exports.GetFramebuffer( instance, &framebufferInfo ) ) { + CopyFramebuffer( framebufferInfo ); + } + } + + Sys_Sleep( 1 ); + } + + return 0; +} + +void idMinigameInstance::CopyFramebuffer( const iceMinigameFramebuffer_t &source ) { + if ( !source.pixels || source.width <= 0 || source.height <= 0 || source.pitchBytes <= 0 || + source.pixelFormat != ICE_MINIGAME_PIXEL_RGBA8 ) { + return; + } + + const int rowBytes = source.width * 4; + const int requiredBytes = rowBytes * source.height; + + Lock(); + if ( requiredBytes != framebufferBytes ) { + ClearFramebuffer(); + framebuffer = (byte *)Mem_Alloc( requiredBytes ); + framebufferBytes = requiredBytes; + } + + if ( framebuffer ) { + for ( int y = 0; y < source.height; y++ ) { + memcpy( framebuffer + y * rowBytes, source.pixels + y * source.pitchBytes, rowBytes ); + } + framebufferWidth = source.width; + framebufferHeight = source.height; + frameReady = true; + } + Unlock(); +} + +void idMinigameInstance::ClearFramebuffer() { + if ( framebuffer ) { + Mem_Free( framebuffer ); + } + framebuffer = NULL; + framebufferWidth = 0; + framebufferHeight = 0; + framebufferBytes = 0; +} diff --git a/neo/engine/minigame/MinigameHost.h b/neo/engine/minigame/MinigameHost.h new file mode 100644 index 00000000..2141e311 --- /dev/null +++ b/neo/engine/minigame/MinigameHost.h @@ -0,0 +1,73 @@ +/* +=========================================================================== + +IceTech GPL Source Code +Copyright (C) 2026 Justin Marshall + +This file is part of the IceTech GPL Source Code. + +=========================================================================== +*/ + +#ifndef __MINIGAME_HOST_H__ +#define __MINIGAME_HOST_H__ + +#include "Minigame_public.h" + +class idMinigameInstance { +public: + idMinigameInstance(); + ~idMinigameInstance(); + + bool Start( const char *moduleName, const char *imageName, const char *commandLine ); + void Shutdown(); + void SetActive( bool active ); + void QueueEvent( const sysEvent_t *event, int time ); + bool UploadFramebuffer(); + + const char * GetImageName() const { return imageName.c_str(); } + bool IsStarted() const { return instance != NULL; } + bool HasFailed() const { return failed; } + +private: + static unsigned int ThreadProc( void *parm ); + unsigned int RunThread(); + void CopyFramebuffer( const iceMinigameFramebuffer_t &source ); + void ClearFramebuffer(); + void Lock(); + void Unlock(); + + static void PrintCallback( void *userData, const char *text ); + static void SubmitAudioCallback( void *userData, const iceMinigameAudioBuffer_t *audio ); + + INT_PTR moduleHandle; + iceMinigameExport_t exports; + iceMinigameImport_t imports; + iceMinigameInstance_t * instance; + + idStr moduleName; + idStr imageName; + idStr commandLine; + idStr resolvedPath; + idStr importBasePath; + idStr importSavePath; + + xthreadInfo threadInfo; + CRITICAL_SECTION criticalSection; + + bool threadStarted; + volatile bool shutdownRequested; + bool active; + bool activeSent; + bool failed; + bool frameReady; + + byte * framebuffer; + int framebufferWidth; + int framebufferHeight; + int framebufferBytes; + + idList pendingEvents; +}; + +#endif // __MINIGAME_HOST_H__ diff --git a/neo/engine/minigame/Minigame_public.h b/neo/engine/minigame/Minigame_public.h new file mode 100644 index 00000000..4cebbfb5 --- /dev/null +++ b/neo/engine/minigame/Minigame_public.h @@ -0,0 +1,108 @@ +/* +=========================================================================== + +IceTech GPL Source Code +Copyright (C) 2026 Justin Marshall + +This file is part of the IceTech GPL Source Code. + +=========================================================================== +*/ + +#ifndef __MINIGAME_PUBLIC_H__ +#define __MINIGAME_PUBLIC_H__ + +#include + +#define ICE_MINIGAME_API_VERSION 1 +#define ICE_MINIGAME_ENTRY_POINT "GetMinigameAPI" + +#ifdef _WIN32 +#ifdef __cplusplus +#define ICE_MINIGAME_EXPORT extern "C" __declspec(dllexport) +#else +#define ICE_MINIGAME_EXPORT __declspec(dllexport) +#endif +#else +#ifdef __cplusplus +#define ICE_MINIGAME_EXPORT extern "C" +#else +#define ICE_MINIGAME_EXPORT +#endif +#endif + +typedef enum { + ICE_MINIGAME_EVENT_NONE = 0, + ICE_MINIGAME_EVENT_KEY, + ICE_MINIGAME_EVENT_CHAR, + ICE_MINIGAME_EVENT_MOUSE +} iceMinigameEventType_t; + +typedef enum { + ICE_MINIGAME_PIXEL_RGBA8 = 1 +} iceMinigamePixelFormat_t; + +typedef struct iceMinigameInputEvent_s { + int type; + int value; + int value2; + int time; +} iceMinigameInputEvent_t; + +typedef struct iceMinigameFramebuffer_s { + const uint8_t *pixels; + int width; + int height; + int pitchBytes; + int pixelFormat; +} iceMinigameFramebuffer_t; + +typedef struct iceMinigameAudioBuffer_s { + const float *samples; + int frames; + int channels; + int sampleRate; +} iceMinigameAudioBuffer_t; + +typedef void (*iceMinigamePrint_t)( void *userData, const char *text ); +typedef void (*iceMinigameSubmitAudio_t)( void *userData, const iceMinigameAudioBuffer_t *audio ); + +typedef struct iceMinigameImport_s { + int version; + void *userData; + const char *basePath; + const char *savePath; + iceMinigamePrint_t Print; + iceMinigameSubmitAudio_t SubmitAudio; +} iceMinigameImport_t; + +typedef struct iceMinigameInstance_s iceMinigameInstance_t; + +typedef int (*iceMinigameCreateInstance_t)( + const iceMinigameImport_t *imports, + const char *commandLine, + iceMinigameInstance_t **instance ); + +typedef void (*iceMinigameDestroyInstance_t)( iceMinigameInstance_t *instance ); +typedef void (*iceMinigameSetActive_t)( iceMinigameInstance_t *instance, int active ); +typedef void (*iceMinigameQueueEvent_t)( iceMinigameInstance_t *instance, const iceMinigameInputEvent_t *event ); +typedef void (*iceMinigameRunFrame_t)( iceMinigameInstance_t *instance, int elapsedMilliseconds ); +typedef int (*iceMinigameGetFramebuffer_t)( iceMinigameInstance_t *instance, iceMinigameFramebuffer_t *framebuffer ); + +typedef struct iceMinigameExport_s { + int version; + const char *gameName; + int framebufferWidth; + int framebufferHeight; + int preferredSampleRate; + iceMinigameCreateInstance_t CreateInstance; + iceMinigameDestroyInstance_t DestroyInstance; + iceMinigameSetActive_t SetActive; + iceMinigameQueueEvent_t QueueEvent; + iceMinigameRunFrame_t RunFrame; + iceMinigameGetFramebuffer_t GetFramebuffer; +} iceMinigameExport_t; + +typedef int (*iceMinigameGetAPI_t)( const iceMinigameImport_t *imports, iceMinigameExport_t *exports ); + +#endif // __MINIGAME_PUBLIC_H__ diff --git a/neo/engine/renderer/RenderSystem.cpp b/neo/engine/renderer/RenderSystem.cpp index 7fa5749c..3e290064 100644 --- a/neo/engine/renderer/RenderSystem.cpp +++ b/neo/engine/renderer/RenderSystem.cpp @@ -1048,6 +1048,36 @@ bool idRenderSystemLocal::UploadImage( const char *imageName, const byte *data, return true; } +/* +=============== +idRenderSystemLocal::CreateScratchImage +=============== +*/ +bool idRenderSystemLocal::CreateScratchImage( const char *imageName, int width, int height ) { + if ( !imageName || !imageName[0] || width <= 0 || height <= 0 ) { + return false; + } + + idImage *image = globalImages->GetImage( imageName ); + if ( !image ) { + image = globalImages->AllocImage( imageName ); + } + if ( !image ) { + return false; + } + + const int bytes = width * height * 4; + byte *data = (byte *)Mem_Alloc( bytes ); + memset( data, 0, bytes ); + for ( int i = 3; i < bytes; i += 4 ) { + data[i] = 255; + } + + image->GenerateImage( data, width, height, TF_LINEAR, false, TR_CLAMP, TD_DEFAULT ); + Mem_Free( data ); + return true; +} + /* =============== idRenderSystemLocal::LoadImage diff --git a/neo/engine/renderer/RenderSystem.h b/neo/engine/renderer/RenderSystem.h index 071aa08b..5e1e5e56 100644 --- a/neo/engine/renderer/RenderSystem.h +++ b/neo/engine/renderer/RenderSystem.h @@ -278,6 +278,7 @@ public: // texture filter / mipmapping / repeat won't be modified by the upload // returns false if the image wasn't found virtual bool UploadImage( const char *imageName, const byte *data, int width, int height ) = 0; + virtual bool CreateScratchImage( const char *imageName, int width, int height ) = 0; // Loads in a image. virtual void LoadImage(const char* cname, byte** pic, int* width, int* height, ID_TIME_T* timestamp, bool makePowerOf2) = 0; diff --git a/neo/engine/renderer/tr_local.h b/neo/engine/renderer/tr_local.h index 3a673247..247afa7a 100644 --- a/neo/engine/renderer/tr_local.h +++ b/neo/engine/renderer/tr_local.h @@ -731,6 +731,7 @@ public: virtual void UnCrop(); virtual void GetCardCaps( bool &oldCard, bool &nv10or20 ); virtual bool UploadImage( const char *imageName, const byte *data, int width, int height ); + virtual bool CreateScratchImage( const char *imageName, int width, int height ); // Loads in a image. virtual void LoadImage(const char* cname, byte** pic, int* width, int* height, ID_TIME_T* timestamp, bool makePowerOf2); diff --git a/neo/engine/ui/Window.cpp b/neo/engine/ui/Window.cpp index cfd6f0ba..fd067938 100644 --- a/neo/engine/ui/Window.cpp +++ b/neo/engine/ui/Window.cpp @@ -37,6 +37,7 @@ If you have questions concerning this license or the applicable additional terms #include "BindWindow.h" #include "ListWindow.h" #include "RenderWindow.h" +#include "MinigameWindow.h" #include "MarkerWindow.h" #include "FieldWindow.h" @@ -2534,6 +2535,17 @@ bool idWindow::Parse(idParser* src, bool rebuild) { dwt.win = win; drawWindows.Append(dwt); } + else if (token == "minigameDef") { + idMinigameWindow* win = new idMinigameWindow(dc, gui); + SaveExpressionParseState(); + win->Parse(src, rebuild); + RestoreExpressionParseState(); + AddChild(win); + win->SetParent(this); + dwt.simp = NULL; + dwt.win = win; + drawWindows.Append(dwt); + } // // added new onEvent else if (token == "onNamedEvent") {