diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..0960baf9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## About + +Fork of the discontinued [GameplayFootball](https://github.com/BazkieBumpercar/GameplayFootball) C++ football game. The goal is to modernize and keep it building on Linux, macOS, and Windows. The `google_brain` branch contains Google Brain's RL-focused variant as a reference. + +## Build + +**Prerequisites (Linux):** +```bash +sudo apt-get install git cmake build-essential libgl1-mesa-dev libsdl2-dev \ +libsdl2-image-dev libsdl2-ttf-dev libsdl2-gfx-dev libopenal-dev libboost-all-dev \ +libdirectfb-dev libst-dev mesa-utils xvfb x11vnc libsqlite3-dev +``` + +**Prerequisites (macOS):** +```bash +brew install cmake sdl2 sdl2_image sdl2_ttf sdl2_gfx boost openal-soft +``` + +**Build steps (Linux/macOS):** +```bash +cp -R data/. build +cd build +cmake .. +make -j$(nproc) +./gameplayfootball +``` + +> macOS note: the game compiles but does not run yet — rendering must happen on the main thread. + +**Debug build:** +```bash +cd build && cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j$(nproc) +``` + +There is no test suite. Validation is done by running the game. + +## Architecture + +The engine ("Blunted2") is a component/entity system split into static libraries linked into the `gameplayfootball` executable. `CMakeLists.txt` + `sources.cmake` define the library structure. + +**Engine libraries (`blunted2` aggregate):** + +| Library | Path | Role | +|---|---|---| +| `baselib` | `src/base/` | Math, geometry, logging, properties | +| `frameworklib` | `src/framework/` | Task scheduling, worker threads | +| `scenelib` | `src/scene/` | Scene graph (2D/3D), scene objects/resources | +| `systemsgraphicslib` | `src/systems/graphics/` | OpenGL renderer, graphics objects/tasks | +| `systemsaudiolib` | `src/systems/audio/` | OpenAL audio | +| `managerslib` | `src/managers/` | Resource managers | +| `utilslib` / `gui2lib` | `src/utils/` | Utilities, GUI widgets | +| `loaderslib` | `src/loaders/` | Asset loading | +| `typeslib` | `src/types/` | Shared type definitions | + +**Game libraries (link against `blunted2`):** + +| Library | Path | Role | +|---|---|---| +| `gamelib` | `src/onthepitch/` | Match simulation: ball, players, AI, teams, referee | +| `hidlib` | `src/hid/` | Human input device handling | +| `menulib` | `src/menu/` | Game menus | +| `datalib` | `src/data/` | Data loading (configs, databases) | +| `leaguelib` | `src/league/` | League/tournament logic | + +**Entry point:** `src/main.cpp` → `src/blunted.cpp` (`Blunted` class) → `src/gametask.cpp` (`GameTask`). + +**Rendering:** `src/systems/graphics/rendering/opengl_renderer3d.cpp` implements `IRenderer3D`. Shaders live in `data/media/shaders/` (GLSL: `ambient.frag`, `lighting.frag`, `postprocess.frag`, `simple.frag`). The renderer is driven by `GraphicsSystem` / `GraphicsTask` via the scheduler in `src/systems/graphics/scheduler.cpp`. + +**Game data:** `data/` must be copied into `build/` before running (CMake does not do this automatically). Databases are SQLite files under `data/databases/`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fa1eb15..4347cbe2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,7 +42,7 @@ include_directories(${SDL2_TTF_DIRS}) FIND_PACKAGE(SDL2_gfx REQUIRED) include_directories(${SDL2_GFX_DIRS}) -FIND_PACKAGE(Boost REQUIRED COMPONENTS system thread filesystem) +FIND_PACKAGE(Boost REQUIRED COMPONENTS thread filesystem) include_directories(${Boost_INCLUDE_DIR}) FIND_PACKAGE(OpenAL REQUIRED) @@ -98,9 +98,16 @@ add_library(menulib ${MENU_SOURCES} ${MENU_HEADERS}) add_library(datalib ${DATA_SOURCES} ${DATA_HEADERS}) set(LIBRARIES gamelib hidlib menulib datalib leaguelib blunted2 - Boost::filesystem Boost::system Boost::thread SQLite::SQLite3 + Boost::filesystem Boost::thread SQLite::SQLite3 ${SDL2_IMAGE_LIBRARIES} ${SDL2_TTF_LIBRARIES} ${SDL2_GFX_LIBRARIES} ${SDL2_LIBRARIES} ${OPENAL_LIBRARY} dl m ${OPENGL_LIBRARIES}) add_executable(gameplayfootball WIN32 ${CORE_SOURCES} ${CORE_HEADERS}) target_link_libraries(gameplayfootball ${LIBRARIES}) + +if(APPLE) + set_target_properties(gameplayfootball PROPERTIES + BUILD_RPATH "/Library/Frameworks;/opt/homebrew/lib" + INSTALL_RPATH "/Library/Frameworks;/opt/homebrew/lib" + ) +endif() diff --git a/data/media/shaders/ambient.frag b/data/media/shaders/ambient.frag index c3fac4b3..526e1a9f 100755 --- a/data/media/shaders/ambient.frag +++ b/data/media/shaders/ambient.frag @@ -62,9 +62,9 @@ float GetEdge(vec2 pos) { float depths[9]; vec3 normals[9]; for (int i = 0; i < 9; i++) { - float depth = texture2D(map_depth, pos + offsets[i] * 0.001).x; + float depth = texture(map_depth, pos + offsets[i] * 0.001).x; depths[i] = cameraClip.y / (depth - cameraClip.x); - normals[i] = texture2D(map_normal, pos + offsets[i] * 0.001).xyz; + normals[i] = texture(map_normal, pos + offsets[i] * 0.001).xyz; } vec4 deltas1; @@ -117,11 +117,11 @@ void main(void) { texCoord.x /= contextWidth; texCoord.y /= contextHeight; - float depth = texture2D(map_depth, texCoord).x; + float depth = texture(map_depth, texCoord).x; vec3 worldPosition = GetWorldPosition(texCoord, depth); - vec3 base = texture2D(map_albedo, texCoord).xyz; + vec3 base = texture(map_albedo, texCoord).xyz; float brightness = 0.15f;//0.25f; @@ -137,13 +137,13 @@ void main(void) { vec2 noiseScale = vec2(contextWidth / 4.0f, contextHeight / 4.0f); float SSAO_radius = 0.18f;//0.12f; - vec3 normal = texture2D(map_normal, texCoord).xyz; + vec3 normal = texture(map_normal, texCoord).xyz; float SSAO = 0.0; vec3 viewPosition = vec3(viewMatrix * vec4(worldPosition, 1.0f)); vec3 viewNormal = vec3(viewMatrix * vec4(normal, 0.0f)); - vec3 randomVec = -texture2D(map_noise, texCoord * noiseScale).xyz * 2.0f - 1.0f; + vec3 randomVec = -texture(map_noise, texCoord * noiseScale).xyz * 2.0f - 1.0f; vec3 tangent = normalize(randomVec - viewNormal * dot(randomVec, viewNormal)); vec3 bitangent = cross(viewNormal, tangent); mat3 tbn = mat3(tangent, bitangent, viewNormal); @@ -184,7 +184,7 @@ void main(void) { // self-illumination - vec4 aux = texture2D(map_aux, texCoord.st); + vec4 aux = texture(map_aux, texCoord.st); float self_illumination = aux.w; vec3 fragColor = vec3(clamp(base * (1.0 + self_illumination), 0.0, 1.0)); diff --git a/data/media/shaders/lighting.frag b/data/media/shaders/lighting.frag index b1d8909b..03fb9672 100755 --- a/data/media/shaders/lighting.frag +++ b/data/media/shaders/lighting.frag @@ -45,7 +45,7 @@ void main(void) { texCoord.x /= contextWidth; texCoord.y /= contextHeight; - float depth = texture2D(map_depth, texCoord).x; // non-linear (0 .. 1) + float depth = texture(map_depth, texCoord).x; // non-linear (0 .. 1) vec3 worldPosition = GetWorldPosition(texCoord, depth); @@ -57,7 +57,7 @@ void main(void) { vec3 lightDir = normalize(lightPosition - worldPosition.xyz); - vec4 normalshine = texture2D(map_normal, texCoord); + vec4 normalshine = texture(map_normal, texCoord); vec3 normal = normalize(normalshine.xyz); float nDotLD = dot(lightDir, normal); @@ -92,7 +92,7 @@ void main(void) { vec3 eyeToFrag = worldPosition.xyz - cameraPosition; vec3 refl = reflect(normalize(eyeToFrag), normal); - vec4 base = texture2D(map_albedo, texCoord); + vec4 base = texture(map_albedo, texCoord); float spec = base.w; vec3 specularColor = lightColor;// * 0.2f + base.rgb * 0.8f; @@ -127,7 +127,7 @@ void main(void) { } // nice debug - //shaded = pow(texture2D(map_depth, texCoord * 1.1f + vec2(0.05, 0.05)).r, 3.0f) * 3.0f - 0.6f; + //shaded = pow(texture(map_depth, texCoord * 1.1f + vec2(0.05, 0.05)).r, 3.0f) * 3.0f - 0.6f; // how 'seriously' do we take shadows? various options below //shaded *= 0.25; diff --git a/data/media/shaders/postprocess.frag b/data/media/shaders/postprocess.frag index 29725496..bc26b7c4 100755 --- a/data/media/shaders/postprocess.frag +++ b/data/media/shaders/postprocess.frag @@ -59,18 +59,18 @@ void main(void) { texCoord.x /= contextWidth; texCoord.y /= contextHeight; - vec4 accum = texture2D(map_accumulation, texCoord); + vec4 accum = texture(map_accumulation, texCoord); vec3 base = accum.rgb; - vec4 modifier = texture2D(map_modifier, texCoord); + vec4 modifier = texture(map_modifier, texCoord); // edge blur if (modifier.r > 0.0) { vec3 smoothPixel = vec3(0); - smoothPixel += texture2D(map_accumulation, texCoord + vec2(0, 1 / contextHeight)).xyz; - smoothPixel += texture2D(map_accumulation, texCoord + vec2(1 / contextWidth, 0)).xyz; - smoothPixel += texture2D(map_accumulation, texCoord + vec2(0, -1 / contextHeight)).xyz; - smoothPixel += texture2D(map_accumulation, texCoord + vec2(-1 / contextWidth, 0)).xyz; + smoothPixel += texture(map_accumulation, texCoord + vec2(0, 1 / contextHeight)).xyz; + smoothPixel += texture(map_accumulation, texCoord + vec2(1 / contextWidth, 0)).xyz; + smoothPixel += texture(map_accumulation, texCoord + vec2(0, -1 / contextHeight)).xyz; + smoothPixel += texture(map_accumulation, texCoord + vec2(-1 / contextWidth, 0)).xyz; smoothPixel *= 0.25; base = base * (1.0 - modifier.r) + smoothPixel * modifier.r; //base = base * (1.0 - modifier.r) + vec3(0, 0, 0) * modifier.r * 0.5 + smoothPixel * modifier.r * 0.5; // cartooney effect @@ -82,7 +82,7 @@ void main(void) { int SSAO_blurSize = 4; - float SSAO = 0.0f; // texture2D(map_modifier, texCoord).g; + float SSAO = 0.0f; // texture(map_modifier, texCoord).g; vec2 texelSize = 1.0f / vec2(textureSize(map_modifier, 0)); vec2 hlim = vec2(float(-SSAO_blurSize) * 0.5f + 0.5f); @@ -101,7 +101,7 @@ void main(void) { // fog - float depth = texture2D(map_depth, texCoord).x; + float depth = texture(map_depth, texCoord).x; // convert from non-linear to linear float fragDepth = cameraClip.y / (depth - cameraClip.x); diff --git a/data/media/shaders/simple.frag b/data/media/shaders/simple.frag index 6e3b6f55..f4a5ae14 100755 --- a/data/media/shaders/simple.frag +++ b/data/media/shaders/simple.frag @@ -23,23 +23,23 @@ out vec4 stdout2; void main(void) { - vec4 base = texture2D(map_albedo, frag_texcoord.st); + vec4 base = texture(map_albedo, frag_texcoord.st); if (base.a < 0.12) discard; vec3 bump; if (materialbools.x == 1.0f) { - bump = normalize(texture2D(map_normal, frag_texcoord.st).xyz * 2.0 - 1.0); + bump = normalize(texture(map_normal, frag_texcoord.st).xyz * 2.0 - 1.0); } else { bump = vec3(0, 0, 1); } float spec; if (materialbools.y == 1.0f) { - spec = texture2D(map_specular, frag_texcoord.st).x * materialparams.y; + spec = texture(map_specular, frag_texcoord.st).x * materialparams.y; } else { spec = materialparams.y; } float illumination; if (materialbools.z == 1.0f) { - illumination = texture2D(map_illumination, frag_texcoord.st).x; + illumination = texture(map_illumination, frag_texcoord.st).x; } else { illumination = materialparams.z; } diff --git a/src/main.cpp b/src/main.cpp index 9f645388..354057e1 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,11 @@ // written by bastiaan konings schuiling 2008 - 2015 // this work is public domain. the code is undocumented, scruffy, untested, and should generally not be used for anything important. // i do not offer support, so don't ask. to be used for inspiration :) +// +// main.cpp — program entry point and global state. +// Owns all top-level singletons (systems, scenes, tasks, controllers) and wires +// them together before handing control to the scheduler. On exit it tears them +// down in reverse order. #ifdef WIN32 #include @@ -43,20 +48,23 @@ using namespace blunted; GraphicsSystem *graphicsSystem; AudioSystem *audioSystem; -boost::shared_ptr scene2D; -boost::shared_ptr scene3D; +boost::shared_ptr scene2D; // 2D overlay scene (HUD, menus, debug images) +boost::shared_ptr scene3D; // 3D world scene (pitch, players, ball) +// Two scheduler sequences run concurrently: game logic and rendering. boost::shared_ptr graphicsSequence; boost::shared_ptr gameSequence; boost::shared_ptr gameTask; boost::shared_ptr menuTask; +// Coloured marker objects placed anywhere in the 3D scene to visualise AI/physics points. boost::intrusive_ptr greenPilon; boost::intrusive_ptr bluePilon; boost::intrusive_ptr yellowPilon; boost::intrusive_ptr redPilon; +// Additional debug shapes for visualising radii or areas of interest. boost::intrusive_ptr smallDebugCircle1; boost::intrusive_ptr smallDebugCircle2; boost::intrusive_ptr largeDebugCircle; @@ -79,21 +87,21 @@ boost::intrusive_ptr GetSmallDebugCircle1() { return smallDebugCircle1 boost::intrusive_ptr GetSmallDebugCircle2() { return smallDebugCircle2; } boost::intrusive_ptr GetLargeDebugCircle() { return largeDebugCircle; } -Database *db; +Database *db; // SQLite player/team database +Properties *config; // key-value config loaded from football.config (or argv[1]) -Properties *config; +// Full-screen 2D surfaces used for runtime debug rendering (disabled in release builds). +boost::intrusive_ptr debugImage; // small thumbnail in the corner (superDebug mode) +boost::intrusive_ptr debugOverlay; // full-screen overlay (AI debug mode) -boost::intrusive_ptr debugImage; -boost::intrusive_ptr debugOverlay; - -std::vector controllers; +std::vector controllers; // index 0 = keyboard; 1+ = gamepads bool superDebug = false; e_DebugMode debugMode = e_DebugMode_Off; std::string activeSaveDirectory; -std::string configFile = "football.config"; +std::string configFile = "football.config"; // overridden by argv[1] at startup std::string GetConfigFilename() { return configFile; } @@ -174,11 +182,12 @@ void GetDebugOverlayCoord(Match *match, const Vector3 &worldPos, int &x, int &y) y = clamp(y, 0, contextH - 1); } +// Returns how many milliseconds remain until the next predicted frame swap. +// Used by game logic to decide how much work it can still do this frame. int PredictFrameTimeToGo_ms(int frameCount) { int averageFrameTime_ms = GetGraphicsSystem()->GetAverageFrameTime_ms(frameCount); int timeSinceLastSwap_ms = GetGraphicsSystem()->GetTimeSinceLastSwap_ms(); int timeToNextSwapPrediction_ms = averageFrameTime_ms - timeSinceLastSwap_ms; - //printf("super prediction! %i - %i = %i\n", averageFrameTime_ms, timeSinceLastSwap_ms, timeToNextSwapPrediction_ms); timeToNextSwapPrediction_ms = clamp(timeToNextSwapPrediction_ms, 0, 1000); return timeToNextSwapPrediction_ms; } @@ -232,6 +241,8 @@ const std::vector &GetControllers() { return controllers; } +// Background thread that drives the ThreadHud overlay (per-thread timing bars). +// Instantiated only in debug builds when the hud flag is enabled (see main()). class ThreadHudThread : public Thread { public: ThreadHudThread() { @@ -244,7 +255,6 @@ class ThreadHudThread : public Thread { virtual void operator()() { bool quit = false; while (!quit) { - SetState(e_ThreadState_Busy); bool isMessage = false; @@ -258,30 +268,30 @@ class ThreadHudThread : public Thread { hud->Execute(); SetState(e_ThreadState_Idle); - boost::this_thread::yield(); } } protected: ThreadHud *hud; - }; int main(int argc, const char** argv) { + // --- Config ------------------------------------------------------------------ config = new Properties(); if (argc > 1) configFile = argv[1]; config->LoadFile(configFile.c_str()); - Initialize(*config); + Initialize(*config); // engine-level init (logging, SDL, etc.) srand(time(NULL)); - rand(); // mingw32? buggy compiler? first value seems bogus - randomseed(); // for the boost random - fastrandomseed(); + rand(); // discard the first value — MinGW's RNG produces a biased first result + randomseed(); // seed boost::random + fastrandomseed(); // seed the lightweight fast-random helper + // Fixed physics timestep; graphics may render at a different (uncapped) rate. int timeStep_ms = config->GetInt("physics_frametime_ms", 10); @@ -390,8 +400,8 @@ int main(int argc, const char** argv) { geometry.reset(); - // controllers - + // --- Controllers ------------------------------------------------------------- + // Keyboard is always controller 0; any connected joysticks follow. HIDKeyboard *keyboard = new HIDKeyboard(); controllers.push_back(keyboard); for (int i = 0; i < SDL_NumJoysticks(); i++) { @@ -400,66 +410,65 @@ int main(int argc, const char** argv) { } - // sequences + // --- Tasks & scheduler sequences -------------------------------------------- + // The scheduler runs two concurrent TaskSequences: + // gameSequence — fixed-timestep game logic (menu + gameplay) + // graphicsSequence — uncapped render loop (reads game state written by gameTask::Put) + // + // Each task has three phases executed in order: Get (read input/state), + // Process (update), Put (write output/scene data). - boost::mutex graphicsGameMutex; // todo: this mutex seems necessary for visual fluency, doesn't this imply that i'm setting positional stuff during something else than gametask put? (or reading during something else than graphics get) + boost::mutex graphicsGameMutex; // guards scene writes vs. reads across sequences gameTask = boost::shared_ptr(new GameTask()); - // TTF_Font *defaultFont = TTF_OpenFont("media/fonts/archivonarrow/ArchivoNarrow-Regular.ttf", 28); - // TTF_Font *defaultOutlineFont = TTF_OpenFont("media/fonts/archivonarrow/ArchivoNarrow-Regular.ttf", 28); std::string fontfilename = config->Get("font_filename", "media/fonts/alegreya/AlegreyaSansSC-ExtraBold.ttf"); TTF_Font *defaultFont = TTF_OpenFont(fontfilename.c_str(), 32); if (!defaultFont) Log(e_FatalError, "football", "main", "Could not load font " + fontfilename); TTF_Font *defaultOutlineFont = TTF_OpenFont(fontfilename.c_str(), 32); - TTF_SetFontOutline(defaultOutlineFont, 2); + TTF_SetFontOutline(defaultOutlineFont, 2); // outline variant used for legible text over the pitch menuTask = boost::shared_ptr(new MenuTask(5.0f / 4.0f, 0, defaultFont, defaultOutlineFont)); + // Wire the first gamepad's A/B buttons to the menu confirm/back actions. if (controllers.size() > 1) menuTask->SetEventJoyButtons(static_cast(controllers.at(1))->GetControllerMapping(e_ControllerButton_A), static_cast(controllers.at(1))->GetControllerMapping(e_ControllerButton_B)); + // Game sequence: runs at the fixed physics rate. + // Order matters — menu runs before gameplay so UI input is consumed first. gameSequence = boost::shared_ptr(new TaskSequence("game", timeStep_ms, false)); - - // note: the whole locking stuff is now happening from within some of the code, iirc, 't is all very ugly and unclear. sorry - - //gameSequence->AddLockEntry(graphicsGameMutex, e_LockAction_Lock); // ---------- lock ----- - gameSequence->AddUserTaskEntry(menuTask, e_TaskPhase_Get); gameSequence->AddUserTaskEntry(menuTask, e_TaskPhase_Process); gameSequence->AddUserTaskEntry(menuTask, e_TaskPhase_Put); - - //gameSequence->AddLockEntry(graphicsGameMutex, e_LockAction_Unlock); // ---------- unlock --- - gameSequence->AddUserTaskEntry(gameTask, e_TaskPhase_Get); gameSequence->AddUserTaskEntry(gameTask, e_TaskPhase_Process); - -// gameSequence->AddLockEntry(graphicsGameMutex, e_LockAction_Unlock); // ---------- unlock --- - GetScheduler()->RegisterTaskSequence(gameSequence); - - + // Graphics sequence: runs as fast as possible (frametime_ms = 0 means uncapped). + // gameTask::Put flushes scene-object positions so the renderer sees consistent state. graphicsSequence = boost::shared_ptr(new TaskSequence("graphics", config->GetInt("graphics3d_frametime_ms", 0), true)); - graphicsSequence->AddUserTaskEntry(gameTask, e_TaskPhase_Put); - - //graphicsSequence->AddLockEntry(graphicsGameMutex, e_LockAction_Lock); // ---------- lock ----- - graphicsSequence->AddSystemTaskEntry(graphicsSystem, e_TaskPhase_Get); - - //graphicsSequence->AddLockEntry(graphicsGameMutex, e_LockAction_Unlock); // ---------- unlock --- - graphicsSequence->AddSystemTaskEntry(graphicsSystem, e_TaskPhase_Process); graphicsSequence->AddSystemTaskEntry(graphicsSystem, e_TaskPhase_Put); - GetScheduler()->RegisterTaskSequence(graphicsSequence); - // fire! - - Run(); + // --- Run -------------------------------------------------------------------- +#ifdef __APPLE__ + // macOS requires OpenGL and Cocoa event pumping on the main thread. + // The scheduler (game logic) moves to a background thread; the main thread + // drives RunMainLoop() which blocks until the game exits. + { + boost::thread schedulerThread([]() { Run(); }); + graphicsSystem->RunMainLoop(); + schedulerThread.join(); + } +#else + Run(); // blocks until the scheduler stops +#endif - // exit + // --- Shutdown --------------------------------------------------------------- + // Release resources in reverse-construction order. if (SuperDebug()) scene2D->DeleteObject(debugImage); if (GetDebugMode() == e_DebugMode_AI) scene2D->DeleteObject(debugOverlay); diff --git a/src/systems/graphics/graphics_system.cpp b/src/systems/graphics/graphics_system.cpp index 89e8ae7e..17f36cf6 100644 --- a/src/systems/graphics/graphics_system.cpp +++ b/src/systems/graphics/graphics_system.cpp @@ -30,12 +30,37 @@ namespace blunted { ResourceManagerPool::GetInstance().RegisterManager(e_ResourceType_Texture, textureResourceManager); ResourceManagerPool::GetInstance().RegisterManager(e_ResourceType_VertexBuffer, vertexBufferResourceManager); - // start thread for renderer if (config.Get("graphics3d_renderer", "opengl") == "opengl") renderer3DTask = new OpenGLRenderer3D(); width = config.GetInt("context_x", 1280); height = config.GetInt("context_y", 720); bpp = config.GetInt("context_bpp", 32); bool fullscreen = config.GetBool("context_fullscreen", false); + +#ifdef __APPLE__ + // On macOS the SDL video subsystem and the OpenGL context must be created on + // the process main thread (Cocoa / NSApp requirement). We therefore call + // InitSDL() and CreateContext() directly here rather than via the renderer + // thread's message queue. RunMainLoop() must be called from main() to drive + // the event pump and process renderer commands on the main thread. + OpenGLRenderer3D *glRenderer = static_cast(renderer3DTask); + glRenderer->InitSDL(); + bool ctxOK = glRenderer->CreateContext(width, height, bpp, fullscreen); + if (!ctxOK) { + Log(e_FatalError, "GraphicsSystem", "Initialize", "Could not create context"); + } else { + Log(e_Notice, "GraphicsSystem", "Initialize", "Created context, resolution " + int_to_str(width) + " * " + int_to_str(height) + " @ " + int_to_str(bpp) + " bpp"); + } + // The window + GL context were created above on the process main thread + // (Cocoa requirement) and the context was released by CreateContext. Now + // start the renderer on its own thread; it claims the context and drains + // its message queue. Starting it here (rather than deferring all GL to the + // main thread) is essential: the main thread blocks on the renderer queue + // during scene/resource setup, so the renderer must already be running to + // service those requests. SDL window events are pumped on the main thread + // via RunMainLoop(). + renderer3DTask->Run(); +#else + // Linux / Windows: renderer runs in its own thread. renderer3DTask->Run(); boost::intrusive_ptr createContext(new Renderer3DMessage_CreateContext(width, height, bpp, fullscreen)); @@ -47,17 +72,17 @@ namespace blunted { } else { Log(e_Notice, "GraphicsSystem", "Initialize", "Created context, resolution " + int_to_str(width) + " * " + int_to_str(height) + " @ " + int_to_str(bpp) + " bpp"); } +#endif task = new GraphicsTask(this); task->Run(); } void GraphicsSystem::Exit() { - // shutdown system task + // Shutdown the GraphicsTask thread (same on all platforms). boost::intrusive_ptr shutdown(new Message_Shutdown()); task->messageQueue.PushMessage(shutdown); shutdown->Wait(); - task->Join(); delete task; task = NULL; @@ -65,16 +90,21 @@ namespace blunted { textureResourceManager.reset(); vertexBufferResourceManager.reset(); - // shutdown renderer thread + // Stop the renderer thread (on all platforms it now runs on its own thread). boost::intrusive_ptr R3Dshutdown(new Message_Shutdown()); renderer3DTask->messageQueue.PushMessage(R3Dshutdown); R3Dshutdown->Wait(); - renderer3DTask->Join(); delete renderer3DTask; renderer3DTask = NULL; } + void GraphicsSystem::RunMainLoop() { +#ifdef __APPLE__ + renderer3DTask->RunLoop(); +#endif + } + e_SystemType GraphicsSystem::GetSystemType() const { return systemType; } diff --git a/src/systems/graphics/graphics_system.hpp b/src/systems/graphics/graphics_system.hpp index 153819a3..7c2c8e46 100644 --- a/src/systems/graphics/graphics_system.hpp +++ b/src/systems/graphics/graphics_system.hpp @@ -54,6 +54,11 @@ namespace blunted { virtual std::string GetName() const { return "graphics"; } + // On macOS the renderer must run on the process main thread. + // Call this from main() instead of Run() to pump SDL events and process + // all renderer messages on the main thread. Returns when the game quits. + void RunMainLoop(); + boost::mutex getPhaseMutex; protected: diff --git a/src/systems/graphics/rendering/interface_renderer3d.hpp b/src/systems/graphics/rendering/interface_renderer3d.hpp index 43057f60..3d4976e4 100644 --- a/src/systems/graphics/rendering/interface_renderer3d.hpp +++ b/src/systems/graphics/rendering/interface_renderer3d.hpp @@ -301,6 +301,11 @@ namespace blunted { virtual void HDRCaptureOverallBrightness() = 0; virtual float HDRGetOverallBrightness() = 0; + // Run the render/event loop on the calling thread (macOS main-thread path). + // Exits when EnvironmentManager signals quit and the message queue drains. + // The default no-op is used on platforms where the renderer runs as a Thread. + virtual void RunLoop() {} + void operator()() = 0; protected: diff --git a/src/systems/graphics/rendering/opengl_renderer3d.cpp b/src/systems/graphics/rendering/opengl_renderer3d.cpp index fe378dd0..3ecb9db6 100644 --- a/src/systems/graphics/rendering/opengl_renderer3d.cpp +++ b/src/systems/graphics/rendering/opengl_renderer3d.cpp @@ -362,16 +362,12 @@ struct GLfunctions { SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1); - //SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - //SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); - //SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); // todo: remember to enable this later on, after migrating to sdl 2 (though it is on by default with most drivers, or so it seems) //SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 1); - -// SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); -// SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); -// SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1); //recently disabled SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1); // wait for vsync? // int SDLerror = SDL_GL_SetSwapInterval(-1); // if (SDLerror == -1) SDL_GL_SetSwapInterval(1); @@ -523,6 +519,16 @@ struct GLfunctions { mapping.glDisable(GL_MULTISAMPLE); +#ifdef __APPLE__ + // On macOS the window + GL context must be created on the process main + // thread (Cocoa requirement), which is where we are now. The renderer, + // however, runs on its own thread (see operator()()), so release the + // context from this thread; the renderer thread claims it via + // SDL_GL_MakeCurrent at the top of its loop. GL objects created above + // (shaders, textures, buffers) belong to the context and stay valid. + SDL_GL_MakeCurrent(window, NULL); +#endif + return true; } @@ -2243,26 +2249,77 @@ struct GLfunctions { // thread main loop - void OpenGLRenderer3D::operator()() { - Log(e_Notice, "OpenGLRenderer3D", "operator()()", "Starting OpenGLRenderer3D thread"); - + void OpenGLRenderer3D::InitSDL() { SDL_Init(SDL_INIT_VIDEO); int flags = IMG_INIT_JPG | IMG_INIT_PNG; int inited = IMG_Init(flags); - if ((inited & flags) != flags) { printf("IMG_Init: Failed to init required jpg and png support!\n"); printf("IMG_Init: %s\n", IMG_GetError()); } + sdlInitialized = true; + } + + // Pumps SDL/Cocoa window events on the process main thread (macOS). All + // OpenGL work and the renderer message queue live on the renderer's own + // thread (operator()()); this loop only feeds input events and watches for + // the quit signal. Returns once the game has signalled quit. + void OpenGLRenderer3D::RunLoop() { + SDL_Event event; + while (!EnvironmentManager::GetInstance().GetQuit()) { + while (SDL_PollEvent(&event)) { + if (event.type == SDL_WINDOWEVENT) { + if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) + contextIsActive = false; + else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) + contextIsActive = true; + } + switch (event.type) { + case SDL_QUIT: + EnvironmentManager::GetInstance().SignalQuit(); + break; + case SDL_KEYDOWN: + if (event.key.keysym.sym == SDLK_F12) + EnvironmentManager::GetInstance().SignalQuit(); + break; + default: + break; + } + if (contextIsActive) + UserEventManager::GetInstance().InputSDLEvent(event); + } + + // Block briefly for the next event so we don't busy-spin the main thread. + SDL_WaitEventTimeout(NULL, 5); + } + } + + void OpenGLRenderer3D::operator()() { + Log(e_Notice, "OpenGLRenderer3D", "operator()()", "Starting OpenGLRenderer3D thread"); + + if (!sdlInitialized) InitSDL(); + +#ifdef __APPLE__ + // The window + GL context were created on the main thread and released + // there (see CreateContext). Claim the context on this, the renderer + // thread, so all subsequent GL work happens here. SDL/Cocoa window events + // are pumped on the main thread (see RunLoop()). + SDL_GL_MakeCurrent(window, context); +#endif + SDL_Event event; + (void)event; bool quit = false; while (!quit) { // process messages +#ifndef __APPLE__ + // On macOS SDL/Cocoa events must be pumped on the process main thread, so + // they are handled in RunLoop() instead of here. while (SDL_PollEvent(&event)) { // context losing/gaining focus @@ -2297,6 +2354,7 @@ struct GLfunctions { } } +#endif // todo: manual joy handling? see joy init in usereventmanager // SDL_JoystickUpdate(); @@ -2312,8 +2370,13 @@ struct GLfunctions { Exit(); +#ifndef __APPLE__ + // On macOS SDL was initialised on the main thread; leave SDL_image / video + // subsystem teardown to process exit rather than tearing it down from this + // secondary thread. IMG_Quit(); SDL_QuitSubSystem(SDL_INIT_VIDEO); +#endif Log(e_Notice, "OpenGLRenderer3D", "operator()()", "Shutting down OpenGLRenderer3D thread"); diff --git a/src/systems/graphics/rendering/opengl_renderer3d.hpp b/src/systems/graphics/rendering/opengl_renderer3d.hpp index 53b5dc6b..a7f8ce89 100644 --- a/src/systems/graphics/rendering/opengl_renderer3d.hpp +++ b/src/systems/graphics/rendering/opengl_renderer3d.hpp @@ -128,9 +128,21 @@ namespace blunted { virtual void HDRCaptureOverallBrightness(); virtual float HDRGetOverallBrightness(); + // Called from the main thread on macOS before starting the game loop. + // Initialises SDL video subsystem and SDL_image (which on macOS must happen + // on the process's main thread so that Cocoa / NSApp is set up correctly). + void InitSDL(); + + // Run the event-pump + renderer message loop on the calling thread. + // Used on macOS where the whole OpenGL pipeline must live on the main thread. + // Returns when EnvironmentManager::GetQuit() is true and the queue is empty. + virtual void RunLoop() override; + void operator()(); protected: + bool sdlInitialized = false; + SDL_GLContext context; SDL_Window* window; int context_width, context_height, context_bpp;