Skip to content
Closed
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
46 changes: 46 additions & 0 deletions .github/workflows/wasm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Optional browser build. Generated with AI (Cursor).
# Started only by hand, so it does not run on other pull requests
# and does not sit next to nightly-builds as a required check.
name: wasm

on:
workflow_dispatch:

jobs:
browser:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install system packages
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build pkg-config curl unzip python3

- name: Setup Emscripten
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git emsdk
./emsdk/emsdk install 3.1.74
./emsdk/emsdk activate 3.1.74

- name: Build web bundle
run: |
source emsdk/emsdk_env.sh
chmod +x scripts/*.sh
./scripts/build-web.sh

- name: Playwright
working-directory: web
run: |
npm run test:e2e:install
npm run test:e2e

- name: Upload screenshot
if: always()
uses: actions/upload-artifact@v4
with:
name: primext-engine-screenshot
path: web/test-results/primext-engine.png
if-no-files-found: warn
18 changes: 17 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ _Pvt_Extensions

# QtCreator
build-*
# Source scripts, not Qt build directories.
!scripts/build-wasm.sh
!scripts/build-web.sh

# Android
*.apk
Expand Down Expand Up @@ -345,4 +348,17 @@ compile_commands.json
.cache/

# KDevelop/Kate project directory metadata
.directory
.directory

# Optional browser port. Build output and demo zips stay local.
/emsdk/
/build-wasm/
/build-wasm-pic/
/web/node_modules/
/web/dist/
/web/test-results/
/web/playwright-report/
/web/public/wasm/
/web/public/xash/
/web/public/primext/
/web/public/test/*.zip
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@ cmake .. --preset linux-x64-debug
cmake --build .
```
6) Compiled binaries will be located in `build` and `build\primext\bin` directories.

## WebAssembly (optional)

An experimental browser build is described in [`web/README.md`](web/README.md). It was generated with AI (Cursor). It is not part of the supported Windows, Linux, or Android workflows, and those builds are left as they were. Drop `web/` and `scripts/build-wasm.sh` if a browser port is out of scope.
17 changes: 17 additions & 0 deletions THIRD_PARTY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Third-party components for the optional browser build

The files added for that build were generated with AI (Cursor).

## Xash3D FWGS

The page uses the npm package `xash3d-fwgs@1.2.2` (MIT) as the engine. PrimeXT is still only the client and server libraries.

## webXash

Browser loading ideas were adapted from [webXash](https://github.com/x8BitRain/webXash) (MIT). No webXash source is copied here.

The test fetches the free Half-Life: Uplink demo zip that webXash redistributes. That zip is not committed. Retail Half-Life assets are not part of this tree.

## Emscripten

https://github.com/emscripten-core/emsdk — used only when building the browser modules.
41 changes: 28 additions & 13 deletions client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,37 @@ file(GLOB GAME_EVENTS_SOURCES "events/*.cpp")
# weapon shared code source files
file(GLOB WEAPONS_SHARED_SOURCES "${CMAKE_SOURCE_DIR}/game_shared/weapons/*.cpp")

# ImGui source files
list(APPEND IMGUI_SOURCES
"ui/vgui_support_int.cpp"
"ui/imgui_manager.cpp"
"ui/imgui_window_system.cpp"
"ui/ui_main.cpp"
"ui/ui_demo_window.cpp"
"ui/ui_material_editor.cpp"
"ui/ui_postfx_menu.cpp"
)
# ImGui / in-game editor UI (Emscripten links stubs instead of Dear ImGui)
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
set(IMGUI_SOURCES
"ui/vgui_support_int.cpp"
"ui/imgui_web_stub.cpp"
)
else()
list(APPEND IMGUI_SOURCES
"ui/vgui_support_int.cpp"
"ui/imgui_manager.cpp"
"ui/imgui_window_system.cpp"
"ui/ui_main.cpp"
"ui/ui_demo_window.cpp"
"ui/ui_material_editor.cpp"
"ui/ui_postfx_menu.cpp"
)
endif()

list(APPEND CLDLL_SOURCES ${IMGUI_SOURCES})
list(APPEND CLDLL_SOURCES ${RENDER_SOURCES})
list(APPEND CLDLL_SOURCES ${ENTITIES_SOURCES})
list(APPEND CLDLL_SOURCES ${GAME_EVENTS_SOURCES})
list(APPEND CLDLL_SOURCES ${WEAPONS_SHARED_SOURCES})
add_library(${PROJECT_NAME} SHARED ${CLDLL_SOURCES})
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
add_library(${PROJECT_NAME} MODULE ${CLDLL_SOURCES})
else()
add_library(${PROJECT_NAME} SHARED ${CLDLL_SOURCES})
endif()
add_dependencies(${PROJECT_NAME} build_info)
include(EmscriptenSideModule)
primext_add_game_module(${PROJECT_NAME} client_emscripten_wasm32)

target_include_directories(${PROJECT_NAME} PRIVATE
"."
Expand Down Expand Up @@ -161,8 +174,10 @@ if(ENABLE_TRACY_PROFILER)
endif()

# link dependency libraries
find_package(imgui CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE imgui::imgui)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
find_package(imgui CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE imgui::imgui)
endif()

find_package(fmt CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE fmt::fmt)
Expand Down
20 changes: 20 additions & 0 deletions client/cdll_int.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ extern "C" int DLLEXPORT HUD_VidInit( void )
{
gHUD.VidInit();
R_VidInit();
#ifndef __EMSCRIPTEN__
g_ImGuiManager.VidInitialize();
#endif
return 1;
}

Expand Down Expand Up @@ -411,6 +413,11 @@ CL_NewMap
Called always when map is changed or restarted
==================
*/
#ifdef __EMSCRIPTEN__
extern "C" void IN_ClientMoveEvent( float forwardmove, float sidemove );
extern "C" void IN_ClientLookEvent( float relyaw, float relpitch );
#endif

void CL_NewMap()
{
// flush collision meshes cache on clientside
Expand Down Expand Up @@ -468,6 +475,19 @@ cldll_func_t cldll_func =
NULL, // HUD_ChatInputPosition,
HUD_GetRenderInterface,
NULL, /// HUD_ClipMoveToEntity
// Tail matches current Xash3D FWGS. Left null outside Emscripten so the
// engine keeps its own mouse/keyboard path on desktop and Android.
#ifdef __EMSCRIPTEN__
NULL, // IN_ClientTouchEvent
IN_ClientMoveEvent,
IN_ClientLookEvent,
#else
NULL,
NULL,
NULL,
#endif
NULL, // HUD_GetSoundInterface
NULL, // Voice_StartChannel
};

extern "C" void DLLEXPORT GetClientAPI( void *pv )
Expand Down
6 changes: 6 additions & 0 deletions client/render/gl_rmisc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,12 @@ void R_VidInit( void )
glState.width = RENDER_GET_PARM( PARM_SCREEN_WIDTH, 0 );
glState.height = RENDER_GET_PARM( PARM_SCREEN_HEIGHT, 0 );

#ifdef __EMSCRIPTEN__
// PrimeXT's GL FBO/shader setup calls renderer entry points the webgl2
// ref does not implement. The engine's own WebGL renderer draws the world.
return;
#endif

COpenGLUnitCube::GetInstance().Initialize();
CDebugVisualizerBackend::GetInstance().Initialize();
CDebugOverlay2D::GetInstance().Initialize();
Expand Down
50 changes: 50 additions & 0 deletions client/ui/imgui_web_stub.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Emscripten-only ImGui stand-in. Part of an AI-generated browser experiment (Cursor).
// Desktop builds do not compile this file.
#include "imgui_manager.h"
#include "imgui_window_system.h"
#include <memory>

class CImGuiBackend {};

CImGuiWindowSystem::CImGuiWindowSystem() = default;
void CImGuiWindowSystem::AddWindow(IImGuiWindow *) {}
void CImGuiWindowSystem::LinkWindows() {}
void CImGuiWindowSystem::Initialize() {}
void CImGuiWindowSystem::VidInitialize() {}
void CImGuiWindowSystem::Terminate() {}
void CImGuiWindowSystem::NewFrame() {}
bool CImGuiWindowSystem::KeyInput(bool, int, const char *) { return false; }
bool CImGuiWindowSystem::CursorRequired() { return false; }

CImGuiManager &g_ImGuiManager = CImGuiManager::GetInstance();

CImGuiManager &CImGuiManager::GetInstance()
{
static CImGuiManager instance;
return instance;
}

void CImGuiManager::Initialize() {}
void CImGuiManager::VidInitialize() {}
void CImGuiManager::Terminate() {}
void CImGuiManager::NewFrame() {}
bool CImGuiManager::KeyInput(bool, int, const char *) { return false; }
void CImGuiManager::TextInputCallback(const char *) {}

CImGuiManager::CImGuiManager() : m_pBackend(std::make_unique<CImGuiBackend>()) {}
CImGuiManager::~CImGuiManager() = default;

void CImGuiManager::LoadFonts() {}
void CImGuiManager::ApplyStyles() {}
void CImGuiManager::UpdateMouseState() {}
void CImGuiManager::UpdateCursorState() {}
void CImGuiManager::UpdateTextInputState() {}
void CImGuiManager::UpdateKeyModifiers() {}
void CImGuiManager::HandleKeyInput(bool, int) {}
bool CImGuiManager::HandleMouseInput(bool, int) { return false; }
void CImGuiManager::SetupConfig() {}
void CImGuiManager::SetupKeyboardMapping() {}
void CImGuiManager::SetupCursorMapping() {}
void CImGuiManager::CheckVguiApiPresence() {}
const char *CImGuiManager::GetClipboardText(void *) { return ""; }
void CImGuiManager::SetClipboardText(void *, const char *) {}
20 changes: 20 additions & 0 deletions cmake/EmscriptenSideModule.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Optional Emscripten helpers. This browser experiment was generated with AI (Cursor).
# vcpkg still archives MODULE libraries; scripts/build-wasm.sh does the final link.
# Native generators never call the body below.

function(primext_add_game_module target)
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
set_target_properties(${target} PROPERTIES
OUTPUT_NAME "${ARGN}"
PREFIX ""
SUFFIX ".wasm"
)
target_link_options(${target} PRIVATE
"SHELL:-s SIDE_MODULE=2"
"SHELL:-s EXPORT_ALL=1"
"SHELL:-s ALLOW_MEMORY_GROWTH=1"
"SHELL:-s IMPORTED_MEMORY=1"
"SHELL:-s ERROR_ON_UNDEFINED_SYMBOLS=0"
)
endif()
endfunction()
8 changes: 8 additions & 0 deletions engine/cdll_exp.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ typedef struct cldll_func_s
// Xash3D extension
int (*pfnGetRenderInterface)( int version, render_api_t *renderfuncs, render_interface_t *callback );
void (*pfnClipMoveToEntity)( struct physent_s *pe, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, struct pmtrace_s *tr );
// Xash3D FWGS extension. Same order as current engine/cdll_exp.h.
// sound_api_t is not vendored in this tree; the two pointer arguments
// match the engine's pfnGetSoundInterface calling convention.
int (*pfnTouchEvent)( int type, int fingerID, float x, float y, float dx, float dy );
void (*pfnMoveEvent)( float forwardmove, float sidemove );
void (*pfnLookEvent)( float relyaw, float relpitch );
int (*pfnGetSoundInterface)( int version, const void *api, void *callback );
int (*pfnVoice_StartChannel)( int samples, byte *data, int entnum );
} cldll_func_t;

#endif//CDLL_EXP_H
5 changes: 4 additions & 1 deletion game_shared/filesystem_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ GNU General Public License for more details.

#include "filesystem_manager.h"
#include "port.h"
#include "build.h"
#include "assert.h"
#include "stringlib.h"
#include <fmt/format.h>
Expand All @@ -33,7 +34,9 @@ bool fs::CFilesystemManager::Initialize()
assert(m_pFileSystem == nullptr);

std::string path;
#if XASH_ANDROID
#if XASH_EMSCRIPTEN
path = "filesystem_stdio.wasm";
#elif XASH_ANDROID
path = fmt::format("{}/lib{}", getenv("XASH3D_ENGLIBDIR"), "filesystem_stdio." OS_LIB_EXT);
#else
path = ("filesystem_stdio." OS_LIB_EXT);
Expand Down
11 changes: 9 additions & 2 deletions game_shared/utlblockmemory.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,15 @@ void CUtlBlockMemory<T,I>::Swap( CUtlBlockMemory< T, I > &mem )
{
SWAP( m_pMemory, mem.m_pMemory );
SWAP( m_nBlocks, mem.m_nBlocks );
SWAP( m_nIndexMask, mem.m_nIndexMask );
SWAP( m_nIndexShift, mem.m_nIndexShift );
// Clang rejects SWAP() on these bit-fields (Emscripten). Same exchange.
{
const int mask = m_nIndexMask;
const int shift = m_nIndexShift;
m_nIndexMask = mem.m_nIndexMask;
m_nIndexShift = mem.m_nIndexShift;
mem.m_nIndexMask = mask;
mem.m_nIndexShift = shift;
}
}


Expand Down
1 change: 1 addition & 0 deletions public/build.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Then you can use another oneliner to query all variables:
#define XASH_WIN32 1
#elif defined __EMSCRIPTEN__
#define XASH_EMSCRIPTEN 1
#define XASH_POSIX 1
#elif defined __WATCOMC__ && defined __DOS__
#define XASH_DOS4GW 1
#else // POSIX compatible
Expand Down
Loading