Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 27906) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 27907) @@ -1,1277 +1,1276 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/GameSetup/GameSetup.h" #include "graphics/GameView.h" #include "graphics/MapReader.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/Scripting/JSInterface_GUIManager.h" #include "i18n/L10n.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/input.h" #include "lib/timer.h" #include "lobby/IXmppClient.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "network/scripting/JSInterface_Network.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/scripting/JSInterface_Game.h" #include "ps/scripting/JSInterface_Main.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/VertexBufferManager.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/JSON.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "simulation2/scripting/JSInterface_Simulation.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); thread_local std::shared_ptr g_ScriptContext; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; size_t priority = 0; for (size_t i = 0; i < mods.size(); ++i) { priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); // Only mount mods from the user path if they don't exist in the 'rdata' path. if (DirectoryExists(modPath / modName / "")) g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; // Mount these dirs with highest priority so that mods can't overwrite them. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); // Mods will be mounted later. // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->Init(); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(HotkeyInputActualHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); // Likewise for the console. in_add_handler(conInputHandler); in_add_handler(touch_input_handler); // Should be called after scancode map update (i.e. after the global input, but before UI). // This never blocks the event, but it does some processing necessary for hotkeys, // which are triggered later down the input chain. // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). in_add_handler(HotkeyInputPrepHandler); // These two must be called first (i.e. pushed last) // GlobalsInputHandler deals with some important global state, // such as which scancodes are being pressed, mouse buttons pressed, etc. // while HotkeyStateChange updates the map of active hotkeys. in_add_handler(GlobalsInputHandler); in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if SDL_VERSION_ATLEAST(2, 0, 14) && OS_WIN // SDL2 >= 2.0.14 Before SDL 2.0.14, this defaulted to true. In 2.0.14 they switched to false // breaking the behavior on Windows. // https://github.com/libsdl-org/SDL/commit/1947ca7028ab165cc3e6cbdb0b4b7c4db68d1710 // https://github.com/libsdl-org/SDL/issues/5033 SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.GetSceneRenderer().ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); - g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); CConfigDB::Shutdown(); TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); debug_filter_add("FILES"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); // On the first Init (INIT_MODS), check for command-line arguments // or use the default mods from the config and enable those. // On later engine restarts (e.g. the mod selector), we will skip this path, // to avoid overwriting the newly selected mods. if (flags & INIT_MODS) { ScriptInterface modInterface("Engine", "Mod", g_ScriptContext); g_Mods.UpdateAvailableMods(modInterface); std::vector mods; if (args.Has("mod")) mods = args.GetMultiple("mod"); else { CStr modsStr; CFG_GET_VAL("mod.enabledmods", modsStr); boost::split(mods, modsStr, boost::algorithm::is_space(), boost::token_compress_on); } if (!g_Mods.EnableMods(mods, flags & INIT_MODS_PUBLIC)) { // In non-visual mode, fail entirely. if (args.Has("autostart-nonvisual")) { LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(g_Mods.GetIncompatibleMods(), ", ")); return false; } } } // If there are incompatible mods, switch to the mod selector so players can resolve the problem. if (g_Mods.GetIncompatibleMods().empty()) MountMods(Paths(args), g_Mods.GetEnabledMods()); else MountMods(Paths(args), { "mod" }); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); debug_printf("Generated entity.rng\n"); return false; } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(!g_Quickstart, g_VideoMode.GetBackendDevice()); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if (RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } g_RenderingOptions.ReadConfigAndSetupHooks(); // create renderer new CRenderer(g_VideoMode.GetBackendDevice()); InitInput(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup std::shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { Script::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) Script::SetProperty(rq, data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } bool InitNonVisual(const CmdLineArgs& args) { return Autostart(args); } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } // TODO: this essentially duplicates the CGUI logic to load directory or scripts. // NB: this won't make sure to not double-load scripts, unlike the GUI. void AutostartLoadScript(const ScriptInterface& scriptInterface, const VfsPath& path) { if (path.IsDirectory()) { VfsPaths pathnames; vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); for (const VfsPath& file : pathnames) scriptInterface.LoadGlobalScriptFile(file); } else scriptInterface.LoadGlobalScriptFile(path); } // TODO: this essentially duplicates the CGUI function CParamNode GetTemplate(const std::string& templateName) { // This is very cheap to create so let's just do it every time. CTemplateLoader templateLoader; const CParamNode& templateRoot = templateLoader.GetTemplateFileData(templateName).GetOnlyChild(); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-biome=BIOME sets BIOME for a random map * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (default 3, 0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only). * Use random for a random civ. * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { if (!args.Has("autostart-client") && !args.Has("autostart")) return false; // Get optional playername. CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Create some scriptinterface to store the js values for the settings. ScriptInterface scriptInterface("Engine", "Game Setup", g_ScriptContext); ScriptRequest rq(scriptInterface); // We use the javascript gameSettings to handle options, but that requires running JS. // Since we don't want to use the full Gui manager, we load an entrypoint script // that can run the priviledged "LoadScript" function, and then call the appropriate function. ScriptFunction::Register<&AutostartLoadScript>(rq, "LoadScript"); // Load the entire folder to allow mods to extend the entrypoint without copying the whole file. AutostartLoadScript(scriptInterface, VfsPath(L"autostart/")); // Provide some required functions to the script. if (args.Has("autostart-nonvisual")) ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); else { JSI_GUIManager::RegisterScriptFunctions(rq); // TODO: this loads pregame, which is hardcoded to exist by various code paths. That ought be changed. InitPs(false, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), JS::UndefinedHandleValue); } JSI_Game::RegisterScriptFunctions(rq); JSI_Main::RegisterScriptFunctions(rq); JSI_Simulation::RegisterScriptFunctions(rq); JSI_VFS::RegisterScriptFunctions_ReadWriteAnywhere(rq); JSI_Network::RegisterScriptFunctions(rq); JS::RootedValue sessionInitData(rq.cx); if (args.Has("autostart-client")) { CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; Script::CreateObject( rq, &sessionInitData, "playerName", userName, "ip", ip, "port", PS_DEFAULT_PORT, "storeReplay", !args.Has("autostart-disable-replay")); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartClient", sessionInitData, true)) return false; bool shouldQuit = false; while (!shouldQuit) { g_NetClient->Poll(); ScriptFunction::Call(rq, global, "onTick", shouldQuit); std::this_thread::sleep_for(std::chrono::microseconds(200)); } if (args.Has("autostart-nonvisual")) { LDR_NonprogressiveLoad(); g_Game->ReallyStartGame(); } return true; } CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); Script::CreateObject(rq, &attrs); Script::CreateObject(rq, &settings); Script::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } Script::SetProperty(rq, settings, "Size", mapSize); // Random map size (in patches) if (args.Has("autostart-biome")) { CStr biome = args.Get("autostart-biome"); Script::SetProperty(rq, settings, "Biome", biome); } // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway Script::CreateObject(rq, &player, "Civ", "athen"); Script::SetPropertyInt(rq, playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios") mapType = "scenario"; else if (mapDirectory == L"skirmishes") mapType = "skirmish"; else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } Script::SetProperty(rq, attrs, "mapType", mapType); Script::SetProperty(rq, attrs, "map", "maps/" + autoStartName); Script::SetProperty(rq, settings, "mapType", mapType); Script::SetProperty(rq, settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } Script::SetProperty(rq, settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } Script::SetProperty(rq, settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (Script::GetPropertyInt(rq, playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; Script::SetProperty(rq, currentPlayer, "Team", teamID); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); Script::SetProperty(rq, settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); Script::SetProperty(rq, currentPlayer, "AI", aiArgs[i].AfterFirst(":")); Script::SetProperty(rq, currentPlayer, "AIDiff", 3); Script::SetProperty(rq, currentPlayer, "AIBehavior", "balanced"); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); Script::SetProperty(rq, currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) Script::CreateObject(rq, ¤tPlayer); Script::SetProperty(rq, currentPlayer, "Civ", civArgs[i].AfterFirst(":")); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (Script::HasProperty(rq, settings, "TriggerScripts")) { Script::GetProperty(rq, settings, "TriggerScripts", &triggerScripts); Script::FromJSVal(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } Script::ToJSVal(rq, &triggerScripts, triggerScriptsVector); Script::SetProperty(rq, settings, "TriggerScripts", triggerScripts); std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); Script::SetProperty(rq, settings, "VictoryConditions", victoryConditions); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); Script::SetProperty(rq, settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); Script::SetProperty(rq, settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); Script::SetProperty(rq, settings, "RelicCount", relicCount); // Add player data to map settings. Script::SetProperty(rq, settings, "PlayerData", playerData); // Add map settings to game attributes. Script::SetProperty(rq, attrs, "settings", settings); if (args.Has("autostart-host")) { int maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerName", userName, "port", PS_DEFAULT_PORT, "maxPlayers", maxPlayers, "storeReplay", !args.Has("autostart-disable-replay")); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartHost", sessionInitData, true)) return false; // In MP host mode, we need to wait until clients have loaded. bool shouldQuit = false; while (!shouldQuit) { g_NetClient->Poll(); ScriptFunction::Call(rq, global, "onTick", shouldQuit); std::this_thread::sleep_for(std::chrono::microseconds(200)); } } else { JS::RootedValue localPlayer(rq.cx); Script::CreateObject( rq, &localPlayer, "player", args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1, "name", userName); JS::RootedValue playerAssignments(rq.cx); Script::CreateObject(rq, &playerAssignments); Script::SetProperty(rq, playerAssignments, "local", localPlayer); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments, "storeReplay", !args.Has("autostart-disable-replay")); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartHost", sessionInitData, false)) return false; } if (args.Has("autostart-nonvisual")) { LDR_NonprogressiveLoad(); g_Game->ReallyStartGame(); } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); JS::RootedValue playerAssignments(rq.cx); Script::CreateObject(rq, &playerAssignments); JS::RootedValue localPlayer(rq.cx); Script::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); Script::SetProperty(rq, playerAssignments, "local", localPlayer); JS::RootedValue sessionInitData(rq.cx); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); return true; } void CancelLoad(const CStrW& message) { std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && Script::HasProperty(rq, global, "cancelOnLoadGameError")) ScriptFunction::CallVoid(rq, global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/renderer/DecalRData.cpp =================================================================== --- ps/trunk/source/renderer/DecalRData.cpp (revision 27906) +++ ps/trunk/source/renderer/DecalRData.cpp (revision 27907) @@ -1,373 +1,373 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "DecalRData.h" #include "graphics/Decal.h" #include "graphics/Model.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "lib/allocators/DynamicArena.h" #include "lib/allocators/STLAllocators.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "renderer/Renderer.h" #include "renderer/TerrainRenderer.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" #include // TODO: Currently each decal is a separate CDecalRData. We might want to use // lots of decals for special effects like shadows, footprints, etc, in which // case we should probably redesign this to batch them all together for more // efficient rendering. namespace { struct SDecalBatch { CDecalRData* decal; CStrIntern shaderEffect; CShaderDefines shaderDefines; CVertexBuffer::VBChunk* vertices; CVertexBuffer::VBChunk* indices; }; struct SDecalBatchComparator { bool operator()(const SDecalBatch& lhs, const SDecalBatch& rhs) const { if (lhs.shaderEffect != rhs.shaderEffect) return lhs.shaderEffect < rhs.shaderEffect; if (lhs.shaderDefines != rhs.shaderDefines) return lhs.shaderDefines < rhs.shaderDefines; const CMaterial& lhsMaterial = lhs.decal->GetDecal()->m_Decal.m_Material; const CMaterial& rhsMaterial = rhs.decal->GetDecal()->m_Decal.m_Material; if (lhsMaterial.GetDiffuseTexture() != rhsMaterial.GetDiffuseTexture()) return lhsMaterial.GetDiffuseTexture() < rhsMaterial.GetDiffuseTexture(); if (lhs.vertices->m_Owner != rhs.vertices->m_Owner) return lhs.vertices->m_Owner < rhs.vertices->m_Owner; if (lhs.indices->m_Owner != rhs.indices->m_Owner) return lhs.indices->m_Owner < rhs.indices->m_Owner; return lhs.decal < rhs.decal; } }; } // anonymous namespace // static Renderer::Backend::IVertexInputLayout* CDecalRData::GetVertexInputLayout() { const uint32_t stride = sizeof(SDecalVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SDecalVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::NORMAL, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SDecalVertex, m_Normal), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SDecalVertex, m_UV), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } CDecalRData::CDecalRData(CModelDecal* decal, CSimulation2* simulation) : m_Decal(decal), m_Simulation(simulation) { BuildVertexData(); } CDecalRData::~CDecalRData() = default; void CDecalRData::Update(CSimulation2* simulation) { m_Simulation = simulation; if (m_UpdateFlags != 0) { BuildVertexData(); m_UpdateFlags = 0; } } void CDecalRData::RenderDecals( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout, const std::vector& decals, const CShaderDefines& context, ShadowMap* shadow) { PROFILE3("render terrain decals"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain decals"); using Arena = Allocators::DynamicArena<256 * KiB>; Arena arena; using Batches = std::vector>; Batches batches((Batches::allocator_type(arena))); batches.reserve(decals.size()); CShaderDefines contextDecal = context; contextDecal.Add(str_DECAL, str_1); for (CDecalRData* decal : decals) { CMaterial& material = decal->m_Decal->m_Decal.m_Material; if (material.GetShaderEffect().empty()) { LOGERROR("Terrain renderer failed to load shader effect.\n"); continue; } if (material.GetSamplers().empty() || !decal->m_VBDecals || !decal->m_VBDecalsIndices) continue; SDecalBatch batch; batch.decal = decal; batch.shaderEffect = material.GetShaderEffect(); batch.shaderDefines = material.GetShaderDefines(); batch.vertices = decal->m_VBDecals.Get(); batch.indices = decal->m_VBDecalsIndices.Get(); batches.emplace_back(std::move(batch)); } if (batches.empty()) return; std::sort(batches.begin(), batches.end(), SDecalBatchComparator()); CVertexBuffer* lastIB = nullptr; for (auto itTechBegin = batches.begin(), itTechEnd = batches.begin(); itTechBegin != batches.end(); itTechBegin = itTechEnd) { while (itTechEnd != batches.end() && itTechBegin->shaderEffect == itTechEnd->shaderEffect && itTechBegin->shaderDefines == itTechEnd->shaderDefines) { ++itTechEnd; } CShaderDefines defines = contextDecal; defines.SetMany(itTechBegin->shaderDefines); // TODO: move enabling blend to XML. CShaderTechniquePtr techBase = g_Renderer.GetShaderManager().LoadEffect( itTechBegin->shaderEffect == str_terrain_base ? str_terrain_decal : itTechBegin->shaderEffect, defines); if (!techBase) { LOGERROR("Terrain renderer failed to load shader effect (%s)\n", itTechBegin->shaderEffect.c_str()); continue; } const int numPasses = techBase->GetNumPasses(); for (int pass = 0; pass < numPasses; ++pass) { deviceCommandContext->SetGraphicsPipelineState( techBase->GetGraphicsPipelineState(pass)); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = techBase->GetShader(pass); TerrainRenderer::PrepareShader(deviceCommandContext, shader, shadow); CColor shadingColor(1.0f, 1.0f, 1.0f, 1.0f); const int32_t shadingColorBindingSlot = shader->GetBindingSlot(str_shadingColor); deviceCommandContext->SetUniform( shadingColorBindingSlot, shadingColor.AsFloatArray()); CShaderUniforms currentStaticUniforms; CVertexBuffer* lastVB = nullptr; for (auto itDecal = itTechBegin; itDecal != itTechEnd; ++itDecal) { SDecalBatch& batch = *itDecal; CDecalRData* decal = batch.decal; CMaterial& material = decal->m_Decal->m_Decal.m_Material; const CMaterial::SamplersVector& samplers = material.GetSamplers(); for (const CMaterial::TextureSampler& sampler : samplers) sampler.Sampler->UploadBackendTextureIfNeeded(deviceCommandContext); for (const CMaterial::TextureSampler& sampler : samplers) { deviceCommandContext->SetTexture( shader->GetBindingSlot(sampler.Name), sampler.Sampler->GetBackendTexture()); } if (currentStaticUniforms != material.GetStaticUniforms()) { currentStaticUniforms = material.GetStaticUniforms(); material.GetStaticUniforms().BindUniforms(deviceCommandContext, shader); } // TODO: Need to handle floating decals correctly. In particular, we need // to render non-floating before water and floating after water (to get // the blending right), and we also need to apply the correct lighting in // each case, which doesn't really seem possible with the current // TerrainRenderer. // Also, need to mark the decals as dirty when water height changes. // m_Decal->GetBounds().Render(); if (shadingColor != decal->m_Decal->GetShadingColor()) { shadingColor = decal->m_Decal->GetShadingColor(); deviceCommandContext->SetUniform( shadingColorBindingSlot, shadingColor.AsFloatArray()); } if (lastVB != batch.vertices->m_Owner) { lastVB = batch.vertices->m_Owner; ENSURE(!lastVB->GetBuffer()->IsDynamic()); deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBuffer( 0, batch.vertices->m_Owner->GetBuffer(), 0); } if (lastIB != batch.indices->m_Owner) { lastIB = batch.indices->m_Owner; ENSURE(!lastIB->GetBuffer()->IsDynamic()); deviceCommandContext->SetIndexBuffer(batch.indices->m_Owner->GetBuffer()); } deviceCommandContext->DrawIndexed(batch.indices->m_Index, batch.indices->m_Count, 0); // bump stats g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += batch.indices->m_Count / 3; } deviceCommandContext->EndPass(); } } } void CDecalRData::BuildVertexData() { PROFILE("decal build"); const SDecal& decal = m_Decal->m_Decal; // TODO: Currently this constructs an axis-aligned bounding rectangle around // the decal. It would be more efficient for rendering if we excluded tiles // that are outside the (non-axis-aligned) decal rectangle. ssize_t i0, j0, i1, j1; m_Decal->CalcVertexExtents(i0, j0, i1, j1); // Currently CalcVertexExtents might return empty rectangle, that means // we can't render it. if (i1 <= i0 || j1 <= j0) { // We have nothing to render. m_VBDecals.Reset(); m_VBDecalsIndices.Reset(); return; } CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); std::vector vertices((i1 - i0 + 1) * (j1 - j0 + 1)); for (ssize_t j = j0, idx = 0; j <= j1; ++j) { for (ssize_t i = i0; i <= i1; ++i, ++idx) { SDecalVertex& vertex = vertices[idx]; m_Decal->m_Terrain->CalcPosition(i, j, vertex.m_Position); if (decal.m_Floating && cmpWaterManager) { vertex.m_Position.Y = std::max( vertex.m_Position.Y, cmpWaterManager->GetExactWaterLevel(vertex.m_Position.X, vertex.m_Position.Z)); } m_Decal->m_Terrain->CalcNormal(i, j, vertex.m_Normal); // Map from world space back into decal texture space. CVector3D inv = m_Decal->GetInvTransform().Transform(vertex.m_Position); vertex.m_UV.X = 0.5f + (inv.X - decal.m_OffsetX) / decal.m_SizeX; // Flip V to match our texture convention. vertex.m_UV.Y = 0.5f - (inv.Z - decal.m_OffsetZ) / decal.m_SizeZ; } } if (!m_VBDecals || m_VBDecals->m_Count != vertices.size()) { - m_VBDecals = g_VBMan.AllocateChunk( + m_VBDecals = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SDecalVertex), vertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false); } m_VBDecals->m_Owner->UpdateChunkVertices(m_VBDecals.Get(), vertices.data()); std::vector indices((i1 - i0) * (j1 - j0) * 6); const ssize_t w = i1 - i0 + 1; auto itIdx = indices.begin(); const size_t base = m_VBDecals->m_Index; for (ssize_t dj = 0; dj < j1 - j0; ++dj) { for (ssize_t di = 0; di < i1 - i0; ++di) { const bool dir = m_Decal->m_Terrain->GetTriangulationDir(i0 + di, j0 + dj); if (dir) { *itIdx++ = u16(((dj + 0) * w + (di + 0)) + base); *itIdx++ = u16(((dj + 0) * w + (di + 1)) + base); *itIdx++ = u16(((dj + 1) * w + (di + 0)) + base); *itIdx++ = u16(((dj + 0) * w + (di + 1)) + base); *itIdx++ = u16(((dj + 1) * w + (di + 1)) + base); *itIdx++ = u16(((dj + 1) * w + (di + 0)) + base); } else { *itIdx++ = u16(((dj + 0) * w + (di + 0)) + base); *itIdx++ = u16(((dj + 0) * w + (di + 1)) + base); *itIdx++ = u16(((dj + 1) * w + (di + 1)) + base); *itIdx++ = u16(((dj + 1) * w + (di + 1)) + base); *itIdx++ = u16(((dj + 1) * w + (di + 0)) + base); *itIdx++ = u16(((dj + 0) * w + (di + 0)) + base); } } } // Construct vertex buffer. if (!m_VBDecalsIndices || m_VBDecalsIndices->m_Count != indices.size()) { - m_VBDecalsIndices = g_VBMan.AllocateChunk( + m_VBDecalsIndices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false); } m_VBDecalsIndices->m_Owner->UpdateChunkVertices(m_VBDecalsIndices.Get(), indices.data()); } Index: ps/trunk/source/renderer/PatchRData.cpp =================================================================== --- ps/trunk/source/renderer/PatchRData.cpp (revision 27906) +++ ps/trunk/source/renderer/PatchRData.cpp (revision 27907) @@ -1,1621 +1,1621 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "renderer/PatchRData.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/Patch.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TextRenderer.h" #include "graphics/TextureManager.h" #include "lib/allocators/DynamicArena.h" #include "lib/allocators/STLAllocators.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "renderer/AlphaMapCalculator.h" #include "renderer/DebugRenderer.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TerrainRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" #include #include #include const ssize_t BlendOffsets[9][2] = { { 0, -1 }, { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 }, { 1, 0 }, { 1, -1 }, { 0, 0 } }; // static Renderer::Backend::IVertexInputLayout* CPatchRData::GetBaseVertexInputLayout() { const uint32_t stride = sizeof(SBaseVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBaseVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::NORMAL, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBaseVertex, m_Normal), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBaseVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } // static Renderer::Backend::IVertexInputLayout* CPatchRData::GetBlendVertexInputLayout() { const uint32_t stride = sizeof(SBlendVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBlendVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::NORMAL, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBlendVertex, m_Normal), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBlendVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SBlendVertex, m_AlphaUVs), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } // static Renderer::Backend::IVertexInputLayout* CPatchRData::GetStreamVertexInputLayout( const bool bindPositionAsTexCoord) { const uint32_t stride = sizeof(SBaseVertex); if (bindPositionAsTexCoord) { const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBaseVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBaseVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } else { const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SBaseVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } } // static Renderer::Backend::IVertexInputLayout* CPatchRData::GetSideVertexInputLayout() { const uint32_t stride = sizeof(SSideVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SSideVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } // static Renderer::Backend::IVertexInputLayout* CPatchRData::GetWaterSurfaceVertexInputLayout( const bool bindWaterData) { const uint32_t stride = sizeof(SWaterVertex); if (bindWaterData) { const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWaterVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, // UV1 will be used only in case of bindWaterData. {Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SWaterVertex, m_WaterData), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } else { const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWaterVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } } // static Renderer::Backend::IVertexInputLayout* CPatchRData::GetWaterShoreVertexInputLayout() { const uint32_t stride = sizeof(SWaterVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWaterVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SWaterVertex, m_WaterData), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } CPatchRData::CPatchRData(CPatch* patch, CSimulation2* simulation) : m_Patch(patch), m_Simulation(simulation) { ENSURE(patch); Build(); } CPatchRData::~CPatchRData() = default; /** * Represents a blend for a single tile, texture and shape. */ struct STileBlend { CTerrainTextureEntry* m_Texture; int m_Priority; u16 m_TileMask; // bit n set if this blend contains neighbour tile BlendOffsets[n] struct DecreasingPriority { bool operator()(const STileBlend& a, const STileBlend& b) const { if (a.m_Priority > b.m_Priority) return true; if (a.m_Priority < b.m_Priority) return false; if (a.m_Texture && b.m_Texture) return a.m_Texture->GetTag() > b.m_Texture->GetTag(); return false; } }; struct CurrentTile { bool operator()(const STileBlend& a) const { return (a.m_TileMask & (1 << 8)) != 0; } }; }; /** * Represents the ordered collection of blends drawn on a particular tile. */ struct STileBlendStack { u8 i, j; std::vector blends; // back of vector is lowest-priority texture }; /** * Represents a batched collection of blends using the same texture. */ struct SBlendLayer { struct Tile { u8 i, j; u8 shape; }; CTerrainTextureEntry* m_Texture; std::vector m_Tiles; }; void CPatchRData::BuildBlends() { PROFILE3("build blends"); m_BlendSplats.clear(); std::vector blendVertices; std::vector blendIndices; CTerrain* terrain = m_Patch->m_Parent; std::vector blendStacks; blendStacks.reserve(PATCH_SIZE*PATCH_SIZE); std::vector blends; blends.reserve(9); // For each tile in patch .. for (ssize_t j = 0; j < PATCH_SIZE; ++j) { for (ssize_t i = 0; i < PATCH_SIZE; ++i) { ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; blends.clear(); // Compute a blend for every tile in the 3x3 square around this tile for (size_t n = 0; n < 9; ++n) { ssize_t ox = gx + BlendOffsets[n][1]; ssize_t oz = gz + BlendOffsets[n][0]; CMiniPatch* nmp = terrain->GetTile(ox, oz); if (!nmp) continue; STileBlend blend; blend.m_Texture = nmp->GetTextureEntry(); blend.m_Priority = nmp->GetPriority(); blend.m_TileMask = 1 << n; blends.push_back(blend); } // Sort the blends, highest priority first std::sort(blends.begin(), blends.end(), STileBlend::DecreasingPriority()); STileBlendStack blendStack; blendStack.i = i; blendStack.j = j; // Put the blends into the tile's stack, merging any adjacent blends with the same texture for (size_t k = 0; k < blends.size(); ++k) { if (!blendStack.blends.empty() && blendStack.blends.back().m_Texture == blends[k].m_Texture) blendStack.blends.back().m_TileMask |= blends[k].m_TileMask; else blendStack.blends.push_back(blends[k]); } // Remove blends that are after (i.e. lower priority than) the current tile // (including the current tile), since we don't want to render them on top of // the tile's base texture blendStack.blends.erase( std::find_if(blendStack.blends.begin(), blendStack.blends.end(), STileBlend::CurrentTile()), blendStack.blends.end()); blendStacks.push_back(blendStack); } } // Given the blend stack per tile, we want to batch together as many blends as possible. // Group them into a series of layers (each of which has a single texture): // (This is effectively a topological sort / linearisation of the partial order induced // by the per-tile stacks, preferring to make tiles with equal textures adjacent.) std::vector blendLayers; while (true) { if (!blendLayers.empty()) { // Try to grab as many tiles as possible that match our current layer, // from off the blend stacks of all the tiles CTerrainTextureEntry* tex = blendLayers.back().m_Texture; for (size_t k = 0; k < blendStacks.size(); ++k) { if (!blendStacks[k].blends.empty() && blendStacks[k].blends.back().m_Texture == tex) { SBlendLayer::Tile t = { blendStacks[k].i, blendStacks[k].j, (u8)blendStacks[k].blends.back().m_TileMask }; blendLayers.back().m_Tiles.push_back(t); blendStacks[k].blends.pop_back(); } // (We've already merged adjacent entries of the same texture in each stack, // so we don't need to bother looping to check the next entry in this stack again) } } // We've grabbed as many tiles as possible; now we need to start a new layer. // The new layer's texture could come from the back of any non-empty stack; // choose the longest stack as a heuristic to reduce the number of layers CTerrainTextureEntry* bestTex = NULL; size_t bestStackSize = 0; for (size_t k = 0; k < blendStacks.size(); ++k) { if (blendStacks[k].blends.size() > bestStackSize) { bestStackSize = blendStacks[k].blends.size(); bestTex = blendStacks[k].blends.back().m_Texture; } } // If all our stacks were empty, we're done if (bestStackSize == 0) break; // Otherwise add the new layer, then loop back and start filling it in SBlendLayer layer; layer.m_Texture = bestTex; blendLayers.push_back(layer); } // Now build outgoing splats m_BlendSplats.resize(blendLayers.size()); for (size_t k = 0; k < blendLayers.size(); ++k) { SSplat& splat = m_BlendSplats[k]; splat.m_IndexStart = blendIndices.size(); splat.m_Texture = blendLayers[k].m_Texture; for (size_t t = 0; t < blendLayers[k].m_Tiles.size(); ++t) { SBlendLayer::Tile& tile = blendLayers[k].m_Tiles[t]; AddBlend(blendVertices, blendIndices, tile.i, tile.j, tile.shape, splat.m_Texture); } splat.m_IndexCount = blendIndices.size() - splat.m_IndexStart; } // Release existing vertex buffer chunks m_VBBlends.Reset(); m_VBBlendIndices.Reset(); if (blendVertices.size()) { // Construct vertex buffer - m_VBBlends = g_VBMan.AllocateChunk( + m_VBBlends = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SBlendVertex), blendVertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBlends->m_Owner->UpdateChunkVertices(m_VBBlends.Get(), &blendVertices[0]); // Update the indices to include the base offset of the vertex data for (size_t k = 0; k < blendIndices.size(); ++k) blendIndices[k] += static_cast(m_VBBlends->m_Index); - m_VBBlendIndices = g_VBMan.AllocateChunk( + m_VBBlendIndices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), blendIndices.size(), Renderer::Backend::IBuffer::Type::INDEX, false, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBlendIndices->m_Owner->UpdateChunkVertices(m_VBBlendIndices.Get(), &blendIndices[0]); } } void CPatchRData::AddBlend(std::vector& blendVertices, std::vector& blendIndices, u16 i, u16 j, u8 shape, CTerrainTextureEntry* texture) { CTerrain* terrain = m_Patch->m_Parent; ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; // uses the current neighbour texture BlendShape8 shape8; for (size_t m = 0; m < 8; ++m) shape8[m] = (shape & (1 << m)) ? 0 : 1; // calculate the required alphamap and the required rotation of the alphamap from blendshape unsigned int alphamapflags; int alphamap = CAlphaMapCalculator::Calculate(shape8, alphamapflags); // now actually render the blend tile (if we need one) if (alphamap == -1) return; float u0 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].u0; float u1 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].u1; float v0 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].v0; float v1 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].v1; if (alphamapflags & BLENDMAP_FLIPU) std::swap(u0, u1); if (alphamapflags & BLENDMAP_FLIPV) std::swap(v0, v1); int base = 0; if (alphamapflags & BLENDMAP_ROTATE90) base = 1; else if (alphamapflags & BLENDMAP_ROTATE180) base = 2; else if (alphamapflags & BLENDMAP_ROTATE270) base = 3; SBlendVertex vtx[4]; vtx[(base + 0) % 4].m_AlphaUVs[0] = u0; vtx[(base + 0) % 4].m_AlphaUVs[1] = v0; vtx[(base + 1) % 4].m_AlphaUVs[0] = u1; vtx[(base + 1) % 4].m_AlphaUVs[1] = v0; vtx[(base + 2) % 4].m_AlphaUVs[0] = u1; vtx[(base + 2) % 4].m_AlphaUVs[1] = v1; vtx[(base + 3) % 4].m_AlphaUVs[0] = u0; vtx[(base + 3) % 4].m_AlphaUVs[1] = v1; SBlendVertex dst; CVector3D normal; u16 index = static_cast(blendVertices.size()); terrain->CalcPosition(gx, gz, dst.m_Position); terrain->CalcNormal(gx, gz, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[0].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[0].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx + 1, gz, dst.m_Position); terrain->CalcNormal(gx + 1, gz, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[1].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[1].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx + 1, gz + 1, dst.m_Position); terrain->CalcNormal(gx + 1, gz + 1, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[2].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[2].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx, gz + 1, dst.m_Position); terrain->CalcNormal(gx, gz + 1, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[3].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[3].m_AlphaUVs[1]; blendVertices.push_back(dst); bool dir = terrain->GetTriangulationDir(gx, gz); if (dir) { blendIndices.push_back(index+0); blendIndices.push_back(index+1); blendIndices.push_back(index+3); blendIndices.push_back(index+1); blendIndices.push_back(index+2); blendIndices.push_back(index+3); } else { blendIndices.push_back(index+0); blendIndices.push_back(index+1); blendIndices.push_back(index+2); blendIndices.push_back(index+2); blendIndices.push_back(index+3); blendIndices.push_back(index+0); } } void CPatchRData::BuildIndices() { PROFILE3("build indices"); CTerrain* terrain = m_Patch->m_Parent; ssize_t px = m_Patch->m_X * PATCH_SIZE; ssize_t pz = m_Patch->m_Z * PATCH_SIZE; // must have allocated some vertices before trying to build corresponding indices ENSURE(m_VBBase); // number of vertices in each direction in each patch ssize_t vsize=PATCH_SIZE+1; // PATCH_SIZE must be 2^8-2 or less to not overflow u16 indices buffer. Thankfully this is always true. ENSURE(vsize*vsize < 65536); std::vector indices; indices.reserve(PATCH_SIZE * PATCH_SIZE * 4); // release existing splats m_Splats.clear(); // build grid of textures on this patch std::vector textures; CTerrainTextureEntry* texgrid[PATCH_SIZE][PATCH_SIZE]; for (ssize_t j=0;jm_MiniPatches[j][i].GetTextureEntry(); texgrid[j][i]=tex; if (std::find(textures.begin(),textures.end(),tex)==textures.end()) { textures.push_back(tex); } } } // now build base splats from interior textures m_Splats.resize(textures.size()); // build indices for base splats size_t base=m_VBBase->m_Index; for (size_t k = 0; k < m_Splats.size(); ++k) { CTerrainTextureEntry* tex = textures[k]; SSplat& splat=m_Splats[k]; splat.m_Texture=tex; splat.m_IndexStart=indices.size(); for (ssize_t j = 0; j < PATCH_SIZE; j++) { for (ssize_t i = 0; i < PATCH_SIZE; i++) { if (texgrid[j][i] == tex) { bool dir = terrain->GetTriangulationDir(px+i, pz+j); if (dir) { indices.push_back(u16(((j+0)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); } else { indices.push_back(u16(((j+0)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+0))+base)); } } } } splat.m_IndexCount=indices.size()-splat.m_IndexStart; } // Release existing vertex buffer chunk m_VBBaseIndices.Reset(); ENSURE(indices.size()); // Construct vertex buffer - m_VBBaseIndices = g_VBMan.AllocateChunk( + m_VBBaseIndices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBaseIndices->m_Owner->UpdateChunkVertices(m_VBBaseIndices.Get(), &indices[0]); } void CPatchRData::BuildVertices() { PROFILE3("build vertices"); // create both vertices and lighting colors // number of vertices in each direction in each patch ssize_t vsize = PATCH_SIZE + 1; std::vector vertices; vertices.resize(vsize * vsize); // get index of this patch ssize_t px = m_Patch->m_X; ssize_t pz = m_Patch->m_Z; CTerrain* terrain = m_Patch->m_Parent; // build vertices for (ssize_t j = 0; j < vsize; ++j) { for (ssize_t i = 0; i < vsize; ++i) { ssize_t ix = px * PATCH_SIZE + i; ssize_t iz = pz * PATCH_SIZE + j; ssize_t v = j * vsize + i; // calculate vertex data terrain->CalcPosition(ix, iz, vertices[v].m_Position); CVector3D normal; terrain->CalcNormal(ix, iz, normal); vertices[v].m_Normal = normal; } } // upload to vertex buffer if (!m_VBBase) { - m_VBBase = g_VBMan.AllocateChunk( + m_VBBase = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SBaseVertex), vsize * vsize, Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::TERRAIN); } m_VBBase->m_Owner->UpdateChunkVertices(m_VBBase.Get(), &vertices[0]); } void CPatchRData::BuildSide(std::vector& vertices, CPatchSideFlags side) { ssize_t vsize = PATCH_SIZE + 1; CTerrain* terrain = m_Patch->m_Parent; CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); for (ssize_t k = 0; k < vsize; k++) { ssize_t gx = m_Patch->m_X * PATCH_SIZE; ssize_t gz = m_Patch->m_Z * PATCH_SIZE; switch (side) { case CPATCH_SIDE_NEGX: gz += k; break; case CPATCH_SIDE_POSX: gx += PATCH_SIZE; gz += PATCH_SIZE-k; break; case CPATCH_SIDE_NEGZ: gx += PATCH_SIZE-k; break; case CPATCH_SIDE_POSZ: gz += PATCH_SIZE; gx += k; break; } CVector3D pos; terrain->CalcPosition(gx, gz, pos); // Clamp the height to the water level float waterHeight = 0.f; if (cmpWaterManager) waterHeight = cmpWaterManager->GetExactWaterLevel(pos.X, pos.Z); pos.Y = std::max(pos.Y, waterHeight); SSideVertex v0, v1; v0.m_Position = pos; v1.m_Position = pos; v1.m_Position.Y = 0; if (k == 0) { vertices.emplace_back(v1); vertices.emplace_back(v0); } if (k > 0) { const size_t lastIndex = vertices.size() - 1; vertices.emplace_back(v1); vertices.emplace_back(vertices[lastIndex]); vertices.emplace_back(v0); vertices.emplace_back(v1); if (k + 1 < vsize) { vertices.emplace_back(v1); vertices.emplace_back(v0); } } } } void CPatchRData::BuildSides() { PROFILE3("build sides"); std::vector sideVertices; int sideFlags = m_Patch->GetSideFlags(); // If no sides are enabled, we don't need to do anything if (!sideFlags) return; // For each side, generate a tristrip by adding a vertex at ground/water // level and a vertex underneath at height 0. if (sideFlags & CPATCH_SIDE_NEGX) BuildSide(sideVertices, CPATCH_SIDE_NEGX); if (sideFlags & CPATCH_SIDE_POSX) BuildSide(sideVertices, CPATCH_SIDE_POSX); if (sideFlags & CPATCH_SIDE_NEGZ) BuildSide(sideVertices, CPATCH_SIDE_NEGZ); if (sideFlags & CPATCH_SIDE_POSZ) BuildSide(sideVertices, CPATCH_SIDE_POSZ); if (sideVertices.empty()) return; if (!m_VBSides) { - m_VBSides = g_VBMan.AllocateChunk( + m_VBSides = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SSideVertex), sideVertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::DEFAULT); } m_VBSides->m_Owner->UpdateChunkVertices(m_VBSides.Get(), &sideVertices[0]); } void CPatchRData::Build() { BuildVertices(); BuildSides(); BuildIndices(); BuildBlends(); BuildWater(); } void CPatchRData::Update(CSimulation2* simulation) { m_Simulation = simulation; if (m_UpdateFlags!=0) { // TODO,RC 11/04/04 - need to only rebuild necessary bits of renderdata rather // than everything; it's complicated slightly because the blends are dependent // on both vertex and index data BuildVertices(); BuildSides(); BuildIndices(); BuildBlends(); BuildWater(); m_UpdateFlags=0; } } // To minimise the cost of memory allocations, everything used for computing // batches uses a arena allocator. (All allocations are short-lived so we can // just throw away the whole arena at the end of each frame.) using Arena = Allocators::DynamicArena<1 * MiB>; // std::map types with appropriate arena allocators and default comparison operator template using PooledBatchMap = std::map, ProxyAllocator, Arena>>; // Equivalent to "m[k]", when it returns a arena-allocated std::map (since we can't // use the default constructor in that case) template typename M::mapped_type& PooledMapGet(M& m, const typename M::key_type& k, Arena& arena) { return m.insert(std::make_pair(k, typename M::mapped_type(typename M::mapped_type::key_compare(), typename M::mapped_type::allocator_type(arena)) )).first->second; } // Equivalent to "m[k]", when it returns a std::pair of arena-allocated std::vectors template typename M::mapped_type& PooledPairGet(M& m, const typename M::key_type& k, Arena& arena) { return m.insert(std::make_pair(k, std::make_pair( typename M::mapped_type::first_type(typename M::mapped_type::first_type::allocator_type(arena)), typename M::mapped_type::second_type(typename M::mapped_type::second_type::allocator_type(arena)) ))).first->second; } // Each multidraw batch has a list of index counts, and a list of pointers-to-first-indexes using BatchElements = std::pair>, std::vector>>; // Group batches by index buffer using IndexBufferBatches = PooledBatchMap; // Group batches by vertex buffer using VertexBufferBatches = PooledBatchMap; // Group batches by texture using TextureBatches = PooledBatchMap; // Group batches by shaders. using ShaderTechniqueBatches = PooledBatchMap, TextureBatches>; void CPatchRData::RenderBases( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout, const std::vector& patches, const CShaderDefines& context, ShadowMap* shadow) { PROFILE3("render terrain bases"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain bases"); Arena arena; ShaderTechniqueBatches batches(ShaderTechniqueBatches::key_compare(), (ShaderTechniqueBatches::allocator_type(arena))); PROFILE_START("compute batches"); // Collect all the patches' base splats into their appropriate batches for (size_t i = 0; i < patches.size(); ++i) { CPatchRData* patch = patches[i]; for (size_t j = 0; j < patch->m_Splats.size(); ++j) { SSplat& splat = patch->m_Splats[j]; const CMaterial& material = splat.m_Texture->GetMaterial(); if (material.GetShaderEffect().empty()) { LOGERROR("Terrain renderer failed to load shader effect.\n"); continue; } BatchElements& batch = PooledPairGet( PooledMapGet( PooledMapGet( PooledMapGet(batches, std::make_pair(material.GetShaderEffect(), material.GetShaderDefines()), arena), splat.m_Texture, arena ), patch->m_VBBase->m_Owner, arena ), patch->m_VBBaseIndices->m_Owner, arena ); batch.first.push_back(splat.m_IndexCount); batch.second.push_back(patch->m_VBBaseIndices->m_Index + splat.m_IndexStart); } } PROFILE_END("compute batches"); // Render each batch for (ShaderTechniqueBatches::iterator itTech = batches.begin(); itTech != batches.end(); ++itTech) { CShaderDefines defines = context; defines.SetMany(itTech->first.second); CShaderTechniquePtr techBase = g_Renderer.GetShaderManager().LoadEffect( itTech->first.first, defines); const int numPasses = techBase->GetNumPasses(); for (int pass = 0; pass < numPasses; ++pass) { deviceCommandContext->SetGraphicsPipelineState( techBase->GetGraphicsPipelineState(pass)); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = techBase->GetShader(pass); TerrainRenderer::PrepareShader(deviceCommandContext, shader, shadow); const int32_t baseTexBindingSlot = shader->GetBindingSlot(str_baseTex); const int32_t textureTransformBindingSlot = shader->GetBindingSlot(str_textureTransform); TextureBatches& textureBatches = itTech->second; for (TextureBatches::iterator itt = textureBatches.begin(); itt != textureBatches.end(); ++itt) { if (!itt->first->GetMaterial().GetSamplers().empty()) { const CMaterial::SamplersVector& samplers = itt->first->GetMaterial().GetSamplers(); for(const CMaterial::TextureSampler& samp : samplers) samp.Sampler->UploadBackendTextureIfNeeded(deviceCommandContext); for(const CMaterial::TextureSampler& samp : samplers) { deviceCommandContext->SetTexture( shader->GetBindingSlot(samp.Name), samp.Sampler->GetBackendTexture()); } itt->first->GetMaterial().GetStaticUniforms().BindUniforms( deviceCommandContext, shader); float c = itt->first->GetTextureMatrix()[0]; float ms = itt->first->GetTextureMatrix()[8]; deviceCommandContext->SetUniform( textureTransformBindingSlot, c, ms); } else { deviceCommandContext->SetTexture( baseTexBindingSlot, g_Renderer.GetTextureManager().GetErrorTexture()->GetBackendTexture()); } for (VertexBufferBatches::iterator itv = itt->second.begin(); itv != itt->second.end(); ++itv) { ENSURE(!itv->first->GetBuffer()->IsDynamic()); deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBuffer(0, itv->first->GetBuffer(), 0); for (IndexBufferBatches::iterator it = itv->second.begin(); it != itv->second.end(); ++it) { ENSURE(!it->first->GetBuffer()->IsDynamic()); deviceCommandContext->SetIndexBuffer(it->first->GetBuffer()); BatchElements& batch = it->second; for (size_t i = 0; i < batch.first.size(); ++i) deviceCommandContext->DrawIndexed(batch.second[i], batch.first[i], 0); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } } deviceCommandContext->EndPass(); } } } /** * Helper structure for RenderBlends. */ struct SBlendBatch { SBlendBatch(Arena& arena) : m_Batches(VertexBufferBatches::key_compare(), VertexBufferBatches::allocator_type(arena)) { } CTerrainTextureEntry* m_Texture; CShaderTechniquePtr m_ShaderTech; VertexBufferBatches m_Batches; }; /** * Helper structure for RenderBlends. */ struct SBlendStackItem { SBlendStackItem(CVertexBuffer::VBChunk* v, CVertexBuffer::VBChunk* i, const std::vector& s, Arena& arena) : vertices(v), indices(i), splats(s.begin(), s.end(), SplatStack::allocator_type(arena)) { } using SplatStack = std::vector>; CVertexBuffer::VBChunk* vertices; CVertexBuffer::VBChunk* indices; SplatStack splats; }; void CPatchRData::RenderBlends( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout, const std::vector& patches, const CShaderDefines& context, ShadowMap* shadow) { PROFILE3("render terrain blends"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain blends"); Arena arena; using BatchesStack = std::vector>; BatchesStack batches((BatchesStack::allocator_type(arena))); CShaderDefines contextBlend = context; contextBlend.Add(str_BLEND, str_1); PROFILE_START("compute batches"); // Reserve an arbitrary size that's probably big enough in most cases, // to avoid heavy reallocations batches.reserve(256); using BlendStacks = std::vector>; BlendStacks blendStacks((BlendStacks::allocator_type(arena))); blendStacks.reserve(patches.size()); // Extract all the blend splats from each patch for (size_t i = 0; i < patches.size(); ++i) { CPatchRData* patch = patches[i]; if (!patch->m_BlendSplats.empty()) { blendStacks.push_back(SBlendStackItem(patch->m_VBBlends.Get(), patch->m_VBBlendIndices.Get(), patch->m_BlendSplats, arena)); // Reverse the splats so the first to be rendered is at the back of the list std::reverse(blendStacks.back().splats.begin(), blendStacks.back().splats.end()); } } // Rearrange the collection of splats to be grouped by texture, preserving // order of splats within each patch: // (This is exactly the same algorithm used in CPatchRData::BuildBlends, // but applied to patch-sized splats rather than to tile-sized splats; // see that function for comments on the algorithm.) while (true) { if (!batches.empty()) { CTerrainTextureEntry* tex = batches.back().m_Texture; for (size_t k = 0; k < blendStacks.size(); ++k) { SBlendStackItem::SplatStack& splats = blendStacks[k].splats; if (!splats.empty() && splats.back().m_Texture == tex) { CVertexBuffer::VBChunk* vertices = blendStacks[k].vertices; CVertexBuffer::VBChunk* indices = blendStacks[k].indices; BatchElements& batch = PooledPairGet(PooledMapGet(batches.back().m_Batches, vertices->m_Owner, arena), indices->m_Owner, arena); batch.first.push_back(splats.back().m_IndexCount); batch.second.push_back(indices->m_Index + splats.back().m_IndexStart); splats.pop_back(); } } } CTerrainTextureEntry* bestTex = NULL; size_t bestStackSize = 0; for (size_t k = 0; k < blendStacks.size(); ++k) { SBlendStackItem::SplatStack& splats = blendStacks[k].splats; if (splats.size() > bestStackSize) { bestStackSize = splats.size(); bestTex = splats.back().m_Texture; } } if (bestStackSize == 0) break; SBlendBatch layer(arena); layer.m_Texture = bestTex; if (!bestTex->GetMaterial().GetSamplers().empty()) { CShaderDefines defines = contextBlend; defines.SetMany(bestTex->GetMaterial().GetShaderDefines()); // TODO: move enabling blend to XML. const CStrIntern shaderEffect = bestTex->GetMaterial().GetShaderEffect(); if (shaderEffect != str_terrain_base) ONCE(LOGWARNING("Shader effect '%s' doesn't support semi-transparent terrain rendering.", shaderEffect.c_str())); layer.m_ShaderTech = g_Renderer.GetShaderManager().LoadEffect( shaderEffect == str_terrain_base ? str_terrain_blend : shaderEffect, defines); } batches.push_back(layer); } PROFILE_END("compute batches"); CVertexBuffer* lastVB = nullptr; Renderer::Backend::IShaderProgram* previousShader = nullptr; for (BatchesStack::iterator itTechBegin = batches.begin(), itTechEnd = batches.begin(); itTechBegin != batches.end(); itTechBegin = itTechEnd) { while (itTechEnd != batches.end() && itTechEnd->m_ShaderTech == itTechBegin->m_ShaderTech) ++itTechEnd; const CShaderTechniquePtr& techBase = itTechBegin->m_ShaderTech; const int numPasses = techBase->GetNumPasses(); for (int pass = 0; pass < numPasses; ++pass) { deviceCommandContext->SetGraphicsPipelineState( techBase->GetGraphicsPipelineState(pass)); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = techBase->GetShader(pass); TerrainRenderer::PrepareShader(deviceCommandContext, shader, shadow); Renderer::Backend::ITexture* lastBlendTex = nullptr; const int32_t baseTexBindingSlot = shader->GetBindingSlot(str_baseTex); const int32_t blendTexBindingSlot = shader->GetBindingSlot(str_blendTex); const int32_t textureTransformBindingSlot = shader->GetBindingSlot(str_textureTransform); for (BatchesStack::iterator itt = itTechBegin; itt != itTechEnd; ++itt) { if (itt->m_Texture->GetMaterial().GetSamplers().empty()) continue; if (itt->m_Texture) { const CMaterial::SamplersVector& samplers = itt->m_Texture->GetMaterial().GetSamplers(); for (const CMaterial::TextureSampler& samp : samplers) samp.Sampler->UploadBackendTextureIfNeeded(deviceCommandContext); for (const CMaterial::TextureSampler& samp : samplers) { deviceCommandContext->SetTexture( shader->GetBindingSlot(samp.Name), samp.Sampler->GetBackendTexture()); } Renderer::Backend::ITexture* currentBlendTex = itt->m_Texture->m_TerrainAlpha->second.m_CompositeAlphaMap.get(); if (currentBlendTex != lastBlendTex) { deviceCommandContext->SetTexture( blendTexBindingSlot, currentBlendTex); lastBlendTex = currentBlendTex; } itt->m_Texture->GetMaterial().GetStaticUniforms().BindUniforms(deviceCommandContext, shader); float c = itt->m_Texture->GetTextureMatrix()[0]; float ms = itt->m_Texture->GetTextureMatrix()[8]; deviceCommandContext->SetUniform( textureTransformBindingSlot, c, ms); } else { deviceCommandContext->SetTexture( baseTexBindingSlot, g_Renderer.GetTextureManager().GetErrorTexture()->GetBackendTexture()); } for (VertexBufferBatches::iterator itv = itt->m_Batches.begin(); itv != itt->m_Batches.end(); ++itv) { // Rebind the VB only if it changed since the last batch if (itv->first != lastVB || shader != previousShader) { lastVB = itv->first; previousShader = shader; ENSURE(!itv->first->GetBuffer()->IsDynamic()); deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBuffer(0, itv->first->GetBuffer(), 0); } for (IndexBufferBatches::iterator it = itv->second.begin(); it != itv->second.end(); ++it) { ENSURE(!it->first->GetBuffer()->IsDynamic()); deviceCommandContext->SetIndexBuffer(it->first->GetBuffer()); BatchElements& batch = it->second; for (size_t i = 0; i < batch.first.size(); ++i) deviceCommandContext->DrawIndexed(batch.second[i], batch.first[i], 0); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_BlendSplats++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } } deviceCommandContext->EndPass(); } } } void CPatchRData::RenderStreams( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout, const std::vector& patches) { PROFILE3("render terrain streams"); // Each batch has a list of index counts, and a list of pointers-to-first-indexes using StreamBatchElements = std::pair, std::vector>; // Group batches by index buffer using StreamIndexBufferBatches = std::map; // Group batches by vertex buffer using StreamVertexBufferBatches = std::map; StreamVertexBufferBatches batches; PROFILE_START("compute batches"); // Collect all the patches into their appropriate batches for (const CPatchRData* patch : patches) { StreamBatchElements& batch = batches[patch->m_VBBase->m_Owner][patch->m_VBBaseIndices->m_Owner]; batch.first.push_back(patch->m_VBBaseIndices->m_Count); batch.second.push_back(patch->m_VBBaseIndices->m_Index); } PROFILE_END("compute batches"); deviceCommandContext->SetVertexInputLayout(vertexInputLayout); // Render each batch for (const std::pair& streamBatch : batches) { ENSURE(!streamBatch.first->GetBuffer()->IsDynamic()); deviceCommandContext->SetVertexBuffer(0, streamBatch.first->GetBuffer(), 0); for (const std::pair& batchIndexBuffer : streamBatch.second) { ENSURE(!batchIndexBuffer.first->GetBuffer()->IsDynamic()); deviceCommandContext->SetIndexBuffer(batchIndexBuffer.first->GetBuffer()); const StreamBatchElements& batch = batchIndexBuffer.second; for (size_t i = 0; i < batch.first.size(); ++i) deviceCommandContext->DrawIndexed(batch.second[i], batch.first[i], 0); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } } void CPatchRData::RenderOutline() { CTerrain* terrain = m_Patch->m_Parent; ssize_t gx = m_Patch->m_X * PATCH_SIZE; ssize_t gz = m_Patch->m_Z * PATCH_SIZE; CVector3D pos; std::vector line; for (ssize_t i = 0, j = 0; i <= PATCH_SIZE; ++i) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = PATCH_SIZE, j = 1; j <= PATCH_SIZE; ++j) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = PATCH_SIZE-1, j = PATCH_SIZE; i >= 0; --i) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = 0, j = PATCH_SIZE-1; j >= 0; --j) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } g_Renderer.GetDebugRenderer().DrawLine(line, CColor(0.0f, 0.0f, 1.0f, 1.0f), 0.1f); } void CPatchRData::RenderSides( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout, const std::vector& patches) { PROFILE3("render terrain sides"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain sides"); if (patches.empty()) return; deviceCommandContext->SetVertexInputLayout(vertexInputLayout); CVertexBuffer* lastVB = nullptr; for (CPatchRData* patch : patches) { ENSURE(patch->m_UpdateFlags == 0); if (!patch->m_VBSides) continue; if (lastVB != patch->m_VBSides->m_Owner) { lastVB = patch->m_VBSides->m_Owner; ENSURE(!lastVB->GetBuffer()->IsDynamic()); deviceCommandContext->SetVertexBuffer(0, patch->m_VBSides->m_Owner->GetBuffer(), 0); } deviceCommandContext->Draw(patch->m_VBSides->m_Index, patch->m_VBSides->m_Count); // bump stats g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += patch->m_VBSides->m_Count / 3; } } void CPatchRData::RenderPriorities(CTextRenderer& textRenderer) { CTerrain* terrain = m_Patch->m_Parent; const CCamera& camera = *(g_Game->GetView()->GetCamera()); for (ssize_t j = 0; j < PATCH_SIZE; ++j) { for (ssize_t i = 0; i < PATCH_SIZE; ++i) { ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; CVector3D pos; terrain->CalcPosition(gx, gz, pos); // Move a bit towards the center of the tile pos.X += TERRAIN_TILE_SIZE/4.f; pos.Z += TERRAIN_TILE_SIZE/4.f; float x, y; camera.GetScreenCoordinates(pos, x, y); textRenderer.PrintfAt(x, y, L"%d", m_Patch->m_MiniPatches[j][i].Priority); } } } // // Water build and rendering // // Build vertex buffer for water vertices over our patch void CPatchRData::BuildWater() { PROFILE3("build water"); // Number of vertices in each direction in each patch ENSURE(PATCH_SIZE % water_cell_size == 0); m_VBWater.Reset(); m_VBWaterIndices.Reset(); m_VBWaterShore.Reset(); m_VBWaterIndicesShore.Reset(); m_WaterBounds.SetEmpty(); // We need to use this to access the water manager or we may not have the // actual values but some compiled-in defaults CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); if (!cmpWaterManager) return; // Build data for water std::vector water_vertex_data; std::vector water_indices; u16 water_index_map[PATCH_SIZE+1][PATCH_SIZE+1]; memset(water_index_map, 0xFF, sizeof(water_index_map)); // Build data for shore std::vector water_vertex_data_shore; std::vector water_indices_shore; u16 water_shore_index_map[PATCH_SIZE+1][PATCH_SIZE+1]; memset(water_shore_index_map, 0xFF, sizeof(water_shore_index_map)); const WaterManager& waterManager = g_Renderer.GetSceneRenderer().GetWaterManager(); CPatch* patch = m_Patch; CTerrain* terrain = patch->m_Parent; ssize_t mapSize = terrain->GetVerticesPerSide(); // Top-left coordinates of our patch. ssize_t px = m_Patch->m_X * PATCH_SIZE; ssize_t pz = m_Patch->m_Z * PATCH_SIZE; // To whoever implements different water heights, this is a TODO: water height) float waterHeight = cmpWaterManager->GetExactWaterLevel(0.0f,0.0f); // The 4 points making a water tile. int moves[4][2] = { {0, 0}, {water_cell_size, 0}, {0, water_cell_size}, {water_cell_size, water_cell_size} }; // Where to look for when checking for water for shore tiles. int check[10][2] = { {0, 0}, {water_cell_size, 0}, {water_cell_size*2, 0}, {0, water_cell_size}, {0, water_cell_size*2}, {water_cell_size, water_cell_size}, {water_cell_size*2, water_cell_size*2}, {-water_cell_size, 0}, {0, -water_cell_size}, {-water_cell_size, -water_cell_size} }; // build vertices, uv, and shader varying for (ssize_t z = 0; z < PATCH_SIZE; z += water_cell_size) { for (ssize_t x = 0; x < PATCH_SIZE; x += water_cell_size) { // Check that this tile is close to water bool nearWater = false; for (size_t test = 0; test < 10; ++test) if (terrain->GetVertexGroundLevel(x + px + check[test][0], z + pz + check[test][1]) < waterHeight) nearWater = true; if (!nearWater) continue; // This is actually lying and I should call CcmpTerrain /*if (!terrain->IsOnMap(x+x1, z+z1) && !terrain->IsOnMap(x+x1, z+z1 + water_cell_size) && !terrain->IsOnMap(x+x1 + water_cell_size, z+z1) && !terrain->IsOnMap(x+x1 + water_cell_size, z+z1 + water_cell_size)) continue;*/ for (int i = 0; i < 4; ++i) { if (water_index_map[z+moves[i][1]][x+moves[i][0]] != 0xFFFF) continue; ssize_t xx = x + px + moves[i][0]; ssize_t zz = z + pz + moves[i][1]; SWaterVertex vertex; terrain->CalcPosition(xx,zz, vertex.m_Position); float depth = waterHeight - vertex.m_Position.Y; vertex.m_Position.Y = waterHeight; m_WaterBounds += vertex.m_Position; vertex.m_WaterData = CVector2D(waterManager.m_WindStrength[xx + zz*mapSize], depth); water_index_map[z+moves[i][1]][x+moves[i][0]] = static_cast(water_vertex_data.size()); water_vertex_data.push_back(vertex); } water_indices.push_back(water_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices.push_back(water_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices.push_back(water_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices.push_back(water_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices.push_back(water_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices.push_back(water_index_map[z + moves[2][1]][x + moves[2][0]]); // Check id this tile is partly over land. // If so add a square over the terrain. This is necessary to render waves that go on shore. if (terrain->GetVertexGroundLevel(x+px, z+pz) < waterHeight && terrain->GetVertexGroundLevel(x+px + water_cell_size, z+pz) < waterHeight && terrain->GetVertexGroundLevel(x+px, z+pz+water_cell_size) < waterHeight && terrain->GetVertexGroundLevel(x+px + water_cell_size, z+pz+water_cell_size) < waterHeight) continue; for (int i = 0; i < 4; ++i) { if (water_shore_index_map[z+moves[i][1]][x+moves[i][0]] != 0xFFFF) continue; ssize_t xx = x + px + moves[i][0]; ssize_t zz = z + pz + moves[i][1]; SWaterVertex vertex; terrain->CalcPosition(xx,zz, vertex.m_Position); vertex.m_Position.Y += 0.02f; m_WaterBounds += vertex.m_Position; vertex.m_WaterData = CVector2D(0.0f, -5.0f); water_shore_index_map[z+moves[i][1]][x+moves[i][0]] = static_cast(water_vertex_data_shore.size()); water_vertex_data_shore.push_back(vertex); } if (terrain->GetTriangulationDir(x + px, z + pz)) { water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); } else { water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); } } } // No vertex buffers if no data generated if (!water_indices.empty()) { - m_VBWater = g_VBMan.AllocateChunk( + m_VBWater = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SWaterVertex), water_vertex_data.size(), Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::WATER); m_VBWater->m_Owner->UpdateChunkVertices(m_VBWater.Get(), &water_vertex_data[0]); - m_VBWaterIndices = g_VBMan.AllocateChunk( + m_VBWaterIndices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), water_indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false, nullptr, CVertexBufferManager::Group::WATER); m_VBWaterIndices->m_Owner->UpdateChunkVertices(m_VBWaterIndices.Get(), &water_indices[0]); } if (!water_indices_shore.empty()) { - m_VBWaterShore = g_VBMan.AllocateChunk( + m_VBWaterShore = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SWaterVertex), water_vertex_data_shore.size(), Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::WATER); m_VBWaterShore->m_Owner->UpdateChunkVertices(m_VBWaterShore.Get(), &water_vertex_data_shore[0]); // Construct indices buffer - m_VBWaterIndicesShore = g_VBMan.AllocateChunk( + m_VBWaterIndicesShore = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), water_indices_shore.size(), Renderer::Backend::IBuffer::Type::INDEX, false, nullptr, CVertexBufferManager::Group::WATER); m_VBWaterIndicesShore->m_Owner->UpdateChunkVertices(m_VBWaterIndicesShore.Get(), &water_indices_shore[0]); } } void CPatchRData::RenderWaterSurface( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout) { ASSERT(m_UpdateFlags == 0); if (!m_VBWater) return; ENSURE(!m_VBWater->m_Owner->GetBuffer()->IsDynamic()); ENSURE(!m_VBWaterIndices->m_Owner->GetBuffer()->IsDynamic()); const uint32_t stride = sizeof(SWaterVertex); const uint32_t firstVertexOffset = m_VBWater->m_Index * stride; deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBuffer( 0, m_VBWater->m_Owner->GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_VBWaterIndices->m_Owner->GetBuffer()); deviceCommandContext->DrawIndexed(m_VBWaterIndices->m_Index, m_VBWaterIndices->m_Count, 0); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_WaterTris += m_VBWaterIndices->m_Count / 3; } void CPatchRData::RenderWaterShore( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout) { ASSERT(m_UpdateFlags == 0); if (!m_VBWaterShore) return; ENSURE(!m_VBWaterShore->m_Owner->GetBuffer()->IsDynamic()); ENSURE(!m_VBWaterIndicesShore->m_Owner->GetBuffer()->IsDynamic()); const uint32_t stride = sizeof(SWaterVertex); const uint32_t firstVertexOffset = m_VBWaterShore->m_Index * stride; deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBuffer( 0, m_VBWaterShore->m_Owner->GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_VBWaterIndicesShore->m_Owner->GetBuffer()); deviceCommandContext->DrawIndexed(m_VBWaterIndicesShore->m_Index, m_VBWaterIndicesShore->m_Count, 0); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_WaterTris += m_VBWaterIndicesShore->m_Count / 3; } Index: ps/trunk/source/renderer/Renderer.cpp =================================================================== --- ps/trunk/source/renderer/Renderer.cpp (revision 27906) +++ ps/trunk/source/renderer/Renderer.cpp (revision 27907) @@ -1,905 +1,912 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "Renderer.h" #include "graphics/Canvas2D.h" #include "graphics/CinemaManager.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/ModelDef.h" #include "graphics/TerrainTextureManager.h" #include "i18n/L10n.h" #include "lib/allocators/shared_ptr.h" #include "lib/hash.h" #include "lib/tex/tex.h" #include "gui/GUIManager.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Globals.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Filesystem.h" #include "ps/World.h" #include "ps/ProfileViewer.h" #include "graphics/Camera.h" #include "graphics/FontManager.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/Texture.h" #include "graphics/TextureManager.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "renderer/backend/IDevice.h" #include "renderer/DebugRenderer.h" #include "renderer/PostprocManager.h" #include "renderer/RenderingOptions.h" #include "renderer/RenderModifiers.h" #include "renderer/SceneRenderer.h" #include "renderer/TimeManager.h" #include "renderer/VertexBufferManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #include namespace { size_t g_NextScreenShotNumber = 0; /////////////////////////////////////////////////////////////////////////////////// // CRendererStatsTable - Profile display of rendering stats /** * Class CRendererStatsTable: Implementation of AbstractProfileTable to * display the renderer stats in-game. * * Accesses CRenderer::m_Stats by keeping the reference passed to the * constructor. */ class CRendererStatsTable : public AbstractProfileTable { NONCOPYABLE(CRendererStatsTable); public: CRendererStatsTable(const CRenderer::Stats& st); // Implementation of AbstractProfileTable interface CStr GetName() override; CStr GetTitle() override; size_t GetNumberRows() override; const std::vector& GetColumns() override; CStr GetCellText(size_t row, size_t col) override; AbstractProfileTable* GetChild(size_t row) override; private: /// Reference to the renderer singleton's stats const CRenderer::Stats& Stats; /// Column descriptions std::vector columnDescriptions; enum { Row_DrawCalls = 0, Row_TerrainTris, Row_WaterTris, Row_ModelTris, Row_OverlayTris, Row_BlendSplats, Row_Particles, Row_VBReserved, Row_VBAllocated, Row_TextureMemory, Row_ShadersLoaded, // Must be last to count number of rows NumberRows }; }; // Construction CRendererStatsTable::CRendererStatsTable(const CRenderer::Stats& st) : Stats(st) { columnDescriptions.push_back(ProfileColumn("Name", 230)); columnDescriptions.push_back(ProfileColumn("Value", 100)); } // Implementation of AbstractProfileTable interface CStr CRendererStatsTable::GetName() { return "renderer"; } CStr CRendererStatsTable::GetTitle() { return "Renderer statistics"; } size_t CRendererStatsTable::GetNumberRows() { return NumberRows; } const std::vector& CRendererStatsTable::GetColumns() { return columnDescriptions; } CStr CRendererStatsTable::GetCellText(size_t row, size_t col) { char buf[256]; switch(row) { case Row_DrawCalls: if (col == 0) return "# draw calls"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_DrawCalls); return buf; case Row_TerrainTris: if (col == 0) return "# terrain tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_TerrainTris); return buf; case Row_WaterTris: if (col == 0) return "# water tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_WaterTris); return buf; case Row_ModelTris: if (col == 0) return "# model tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_ModelTris); return buf; case Row_OverlayTris: if (col == 0) return "# overlay tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_OverlayTris); return buf; case Row_BlendSplats: if (col == 0) return "# blend splats"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_BlendSplats); return buf; case Row_Particles: if (col == 0) return "# particles"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_Particles); return buf; case Row_VBReserved: if (col == 0) return "VB reserved"; - sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_VBMan.GetBytesReserved() / 1024); + sprintf_s(buf, sizeof(buf), "%lu kB", static_cast(g_Renderer.GetVertexBufferManager().GetBytesReserved() / 1024)); return buf; case Row_VBAllocated: if (col == 0) return "VB allocated"; - sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_VBMan.GetBytesAllocated() / 1024); + sprintf_s(buf, sizeof(buf), "%lu kB", static_cast(g_Renderer.GetVertexBufferManager().GetBytesAllocated() / 1024)); return buf; case Row_TextureMemory: if (col == 0) return "textures uploaded"; sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_Renderer.GetTextureManager().GetBytesUploaded() / 1024); return buf; case Row_ShadersLoaded: if (col == 0) return "shader effects loaded"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)g_Renderer.GetShaderManager().GetNumEffectsLoaded()); return buf; default: return "???"; } } AbstractProfileTable* CRendererStatsTable::GetChild(size_t UNUSED(row)) { return 0; } } // anonymous namespace /////////////////////////////////////////////////////////////////////////////////// // CRenderer implementation /** * Struct CRendererInternals: Truly hide data that is supposed to be hidden * in this structure so it won't even appear in header files. */ class CRenderer::Internals { NONCOPYABLE(Internals); public: Renderer::Backend::IDevice* device; std::unique_ptr deviceCommandContext; /// true if CRenderer::Open has been called bool IsOpen; /// true if shaders need to be reloaded bool ShadersDirty; /// Table to display renderer stats in-game via profile system CRendererStatsTable profileTable; /// Shader manager CShaderManager shaderManager; /// Texture manager CTextureManager textureManager; + CVertexBufferManager vertexBufferManager; + /// Time manager CTimeManager timeManager; /// Postprocessing effect manager CPostprocManager postprocManager; CSceneRenderer sceneRenderer; CDebugRenderer debugRenderer; CFontManager fontManager; struct VertexAttributesHash { size_t operator()(const std::vector& attributes) const; }; std::unordered_map< std::vector, std::unique_ptr, VertexAttributesHash> vertexInputLayouts; Internals(Renderer::Backend::IDevice* device) : device(device), deviceCommandContext(device->CreateCommandContext()), IsOpen(false), ShadersDirty(true), profileTable(g_Renderer.m_Stats), - shaderManager(device), textureManager(g_VFS, false, device), + shaderManager(device), textureManager(g_VFS, false, device), vertexBufferManager(device), postprocManager(device), sceneRenderer(device) { } }; size_t CRenderer::Internals::VertexAttributesHash::operator()( const std::vector& attributes) const { size_t seed = 0; hash_combine(seed, attributes.size()); for (const Renderer::Backend::SVertexAttributeFormat& attribute : attributes) { hash_combine(seed, attribute.stream); hash_combine(seed, attribute.format); hash_combine(seed, attribute.offset); hash_combine(seed, attribute.stride); hash_combine(seed, attribute.rate); hash_combine(seed, attribute.bindingSlot); } return seed; } CRenderer::CRenderer(Renderer::Backend::IDevice* device) { TIMER(L"InitRenderer"); m = std::make_unique(device); g_ProfileViewer.AddRootTable(&m->profileTable); m_Width = 0; m_Height = 0; m_Stats.Reset(); // Create terrain related stuff. new CTerrainTextureManager(device); Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. GetSceneRenderer().SetLightEnv(&g_LightEnv); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } CRenderer::~CRenderer() { delete &g_TexMan; // We no longer UnloadWaterTextures here - // that is the responsibility of the module that asked for // them to be loaded (i.e. CGameView). m.reset(); } void CRenderer::ReloadShaders() { ENSURE(m->IsOpen); m->sceneRenderer.ReloadShaders(m->device); m->ShadersDirty = false; } bool CRenderer::Open(int width, int height) { m->IsOpen = true; // Dimensions m_Width = width; m_Height = height; // Validate the currently selected render path SetRenderPath(g_RenderingOptions.GetRenderPath()); m->debugRenderer.Initialize(); if (m->postprocManager.IsEnabled()) m->postprocManager.Initialize(); m->sceneRenderer.Initialize(); return true; } void CRenderer::Resize(int width, int height) { m_Width = width; m_Height = height; m->postprocManager.Resize(); m->sceneRenderer.Resize(width, height); } void CRenderer::SetRenderPath(RenderPath rp) { if (!m->IsOpen) { // Delay until Open() is called. return; } // Renderer has been opened, so validate the selected renderpath const bool hasShadersSupport = m->device->GetCapabilities().ARBShaders || m->device->GetBackend() != Renderer::Backend::Backend::GL_ARB; if (rp == RenderPath::DEFAULT) { if (hasShadersSupport) rp = RenderPath::SHADER; else rp = RenderPath::FIXED; } if (rp == RenderPath::SHADER) { if (!hasShadersSupport) { LOGWARNING("Falling back to fixed function\n"); rp = RenderPath::FIXED; } } // TODO: remove this once capabilities have been properly extracted and the above checks have been moved elsewhere. g_RenderingOptions.m_RenderPath = rp; MakeShadersDirty(); } bool CRenderer::ShouldRender() const { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void CRenderer::RenderFrame(const bool needsPresent) { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; if (m_ScreenShotType == ScreenShotType::BIG) { RenderBigScreenShot(needsPresent); } else if (m_ScreenShotType == ScreenShotType::DEFAULT) { RenderScreenShot(needsPresent); } else { if (needsPresent) { // In case of no acquired backbuffer we have nothing render to. if (!m->device->AcquireNextBackbuffer()) return; } if (m_ShouldPreloadResourcesBeforeNextFrame) { m_ShouldPreloadResourcesBeforeNextFrame = false; // We don't need to render logger for the preload. RenderFrameImpl(true, false); } RenderFrameImpl(true, true); m->deviceCommandContext->Flush(); if (needsPresent) m->device->Present(); } } void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger) { PROFILE3("render"); g_Profiler2.RecordGPUFrameStart(); g_TexMan.UploadResourcesIfNeeded(m->deviceCommandContext.get()); m->textureManager.MakeUploadProgress(m->deviceCommandContext.get()); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) m->sceneRenderer.SetSimulation(g_Game->GetSimulation2()); // start new frame BeginFrame(); if (g_Game && g_Game->IsGameStarted()) { g_Game->GetView()->Prepare(m->deviceCommandContext.get()); Renderer::Backend::IFramebuffer* framebuffer = nullptr; CPostprocManager& postprocManager = GetPostprocManager(); if (postprocManager.IsEnabled()) { // We have to update the post process manager with real near/far planes // that we use for the scene rendering. postprocManager.SetDepthBufferClipPlanes( m->sceneRenderer.GetViewCamera().GetNearPlane(), m->sceneRenderer.GetViewCamera().GetFarPlane() ); postprocManager.Initialize(); framebuffer = postprocManager.PrepareAndGetOutputFramebuffer(); } else { // We don't need to clear the color attachment of the framebuffer as the sky // is going to be rendered anyway. framebuffer = m->deviceCommandContext->GetDevice()->GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::DONT_CARE, Renderer::Backend::AttachmentStoreOp::STORE, Renderer::Backend::AttachmentLoadOp::CLEAR, Renderer::Backend::AttachmentStoreOp::DONT_CARE); } m->deviceCommandContext->BeginFramebufferPass(framebuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = m_Width; viewportRect.height = m_Height; m->deviceCommandContext->SetViewports(1, &viewportRect); g_Game->GetView()->Render(m->deviceCommandContext.get()); if (postprocManager.IsEnabled()) { m->deviceCommandContext->EndFramebufferPass(); if (postprocManager.IsMultisampleEnabled()) postprocManager.ResolveMultisampleFramebuffer(m->deviceCommandContext.get()); postprocManager.ApplyPostproc(m->deviceCommandContext.get()); Renderer::Backend::IFramebuffer* backbuffer = m->deviceCommandContext->GetDevice()->GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::LOAD, Renderer::Backend::AttachmentStoreOp::STORE, Renderer::Backend::AttachmentLoadOp::LOAD, Renderer::Backend::AttachmentStoreOp::DONT_CARE); postprocManager.BlitOutputFramebuffer( m->deviceCommandContext.get(), backbuffer); m->deviceCommandContext->BeginFramebufferPass(backbuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = m_Width; viewportRect.height = m_Height; m->deviceCommandContext->SetViewports(1, &viewportRect); } g_Game->GetView()->RenderOverlays(m->deviceCommandContext.get()); g_Game->GetView()->GetCinema()->Render(); } else { // We have a fullscreen background in our UI so we don't need // to clear the color attachment. // We don't need a depth test to render so we don't care about the // depth-stencil attachment content. // In case of Atlas we don't have g_Game, so we still need to clear depth. const Renderer::Backend::AttachmentLoadOp depthStencilLoadOp = g_AtlasGameLoop && g_AtlasGameLoop->view ? Renderer::Backend::AttachmentLoadOp::CLEAR : Renderer::Backend::AttachmentLoadOp::DONT_CARE; Renderer::Backend::IFramebuffer* backbuffer = m->deviceCommandContext->GetDevice()->GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::DONT_CARE, Renderer::Backend::AttachmentStoreOp::STORE, depthStencilLoadOp, Renderer::Backend::AttachmentStoreOp::DONT_CARE); m->deviceCommandContext->BeginFramebufferPass(backbuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = m_Width; viewportRect.height = m_Height; m->deviceCommandContext->SetViewports(1, &viewportRect); } // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); } RenderFrame2D(renderGUI, renderLogger); m->deviceCommandContext->EndFramebufferPass(); EndFrame(); const Stats& stats = GetStats(); PROFILE2_ATTR("draw calls: %zu", stats.m_DrawCalls); PROFILE2_ATTR("terrain tris: %zu", stats.m_TerrainTris); PROFILE2_ATTR("water tris: %zu", stats.m_WaterTris); PROFILE2_ATTR("model tris: %zu", stats.m_ModelTris); PROFILE2_ATTR("overlay tris: %zu", stats.m_OverlayTris); PROFILE2_ATTR("blend splats: %zu", stats.m_BlendSplats); PROFILE2_ATTR("particles: %zu", stats.m_Particles); g_Profiler2.RecordGPUFrameEnd(); } void CRenderer::RenderFrame2D(const bool renderGUI, const bool renderLogger) { CCanvas2D canvas(g_xres, g_yres, g_VideoMode.GetScale(), m->deviceCommandContext.get()); m->sceneRenderer.RenderTextOverlays(canvas); if (renderGUI) { GPU_SCOPED_LABEL(m->deviceCommandContext.get(), "Render GUI"); // All GUI elements are drawn in Z order to render semi-transparent // objects correctly. g_GUI->Draw(canvas); } // If we're in Atlas game view, render special overlays (e.g. editor bandbox). if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(canvas); } { GPU_SCOPED_LABEL(m->deviceCommandContext.get(), "Render console"); g_Console->Render(canvas); } if (renderLogger) { GPU_SCOPED_LABEL(m->deviceCommandContext.get(), "Render logger"); g_Logger->Render(canvas); } { GPU_SCOPED_LABEL(m->deviceCommandContext.get(), "Render profiler"); // Profile information g_ProfileViewer.RenderProfile(canvas); } } void CRenderer::RenderScreenShot(const bool needsPresent) { m_ScreenShotType = ScreenShotType::NONE; // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. const VfsPath filenameFormat(L"screenshots/screenshot%04d.png"); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename); const size_t width = static_cast(g_xres), height = static_cast(g_yres); const size_t bpp = 24; if (needsPresent && !m->device->AcquireNextBackbuffer()) return; // Hide log messages and re-render RenderFrameImpl(true, false); const size_t img_size = width * height * bpp / 8; const size_t hdr_size = tex_hdr_size(filename); std::shared_ptr buf; AllocateAligned(buf, hdr_size + img_size, maxSectorSize); void* img = buf.get() + hdr_size; Tex t; if (t.wrap(width, height, bpp, TEX_BOTTOM_UP, buf, hdr_size) < 0) return; m->deviceCommandContext->ReadbackFramebufferSync(0, 0, width, height, img); m->deviceCommandContext->Flush(); if (needsPresent) m->device->Present(); if (tex_write(&t, filename) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); debug_printf( CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); } void CRenderer::RenderBigScreenShot(const bool needsPresent) { m_ScreenShotType = ScreenShotType::NONE; // If the game hasn't started yet then use WriteScreenshot to generate the image. if (!g_Game) return RenderScreenShot(needsPresent); int tiles = 4, tileWidth = 256, tileHeight = 256; CFG_GET_VAL("screenshot.tiles", tiles); CFG_GET_VAL("screenshot.tilewidth", tileWidth); CFG_GET_VAL("screenshot.tileheight", tileHeight); if (tiles <= 0 || tileWidth <= 0 || tileHeight <= 0 || tileWidth * tiles % 4 != 0 || tileHeight * tiles % 4 != 0) { LOGWARNING("Invalid big screenshot size: tiles=%d tileWidth=%d tileHeight=%d", tiles, tileWidth, tileHeight); return; } if (g_xres < tileWidth && g_yres < tileHeight) { LOGWARNING( "The window size is too small for a big screenshot, increase the" " window size %dx%d or decrease the tile size %dx%d", g_xres, g_yres, tileWidth, tileHeight); return; } // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. const VfsPath filenameFormat(L"screenshots/screenshot%04d.bmp"); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename); const int imageWidth = tileWidth * tiles, imageHeight = tileHeight * tiles; const int bpp = 24; const size_t imageSize = imageWidth * imageHeight * bpp / 8; const size_t tileSize = tileWidth * tileHeight * bpp / 8; const size_t headerSize = tex_hdr_size(filename); void* tileData = malloc(tileSize); if (!tileData) { WARN_IF_ERR(ERR::NO_MEM); return; } std::shared_ptr imageBuffer; AllocateAligned(imageBuffer, headerSize + imageSize, maxSectorSize); Tex t; void* img = imageBuffer.get() + headerSize; if (t.wrap(imageWidth, imageHeight, bpp, TEX_BOTTOM_UP, imageBuffer, headerSize) < 0) { free(tileData); return; } CCamera oldCamera = *g_Game->GetView()->GetCamera(); // Resize various things so that the sizes and aspect ratios are correct { g_Renderer.Resize(tileWidth, tileHeight); SViewPort vp = { 0, 0, tileWidth, tileHeight }; g_Game->GetView()->SetViewport(vp); } // Render each tile CMatrix3D projection; projection.SetIdentity(); const float aspectRatio = 1.0f * tileWidth / tileHeight; for (int tileY = 0; tileY < tiles; ++tileY) { for (int tileX = 0; tileX < tiles; ++tileX) { // Adjust the camera to render the appropriate region if (oldCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE) { projection.SetPerspectiveTile( oldCamera.GetFOV(), aspectRatio, oldCamera.GetNearPlane(), oldCamera.GetFarPlane(), tiles, tileX, tileY); } g_Game->GetView()->GetCamera()->SetProjection(projection); if (!needsPresent || m->device->AcquireNextBackbuffer()) { RenderFrameImpl(false, false); m->deviceCommandContext->ReadbackFramebufferSync(0, 0, tileWidth, tileHeight, tileData); m->deviceCommandContext->Flush(); if (needsPresent) m->device->Present(); } // Copy the tile pixels into the main image for (int y = 0; y < tileHeight; ++y) { void* dest = static_cast(img) + ((tileY * tileHeight + y) * imageWidth + (tileX * tileWidth)) * bpp / 8; void* src = static_cast(tileData) + y * tileWidth * bpp / 8; memcpy(dest, src, tileWidth * bpp / 8); } } } // Restore the viewport settings { g_Renderer.Resize(g_xres, g_yres); SViewPort vp = { 0, 0, g_xres, g_yres }; g_Game->GetView()->SetViewport(vp); g_Game->GetView()->GetCamera()->SetProjectionFromCamera(oldCamera); } if (tex_write(&t, filename) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); debug_printf( CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); free(tileData); } void CRenderer::BeginFrame() { PROFILE("begin frame"); // Zero out all the per-frame stats. m_Stats.Reset(); if (m->ShadersDirty) ReloadShaders(); m->sceneRenderer.BeginFrame(); } void CRenderer::EndFrame() { PROFILE3("end frame"); m->sceneRenderer.EndFrame(); } void CRenderer::MakeShadersDirty() { m->ShadersDirty = true; m->sceneRenderer.MakeShadersDirty(); } CTextureManager& CRenderer::GetTextureManager() { return m->textureManager; } +CVertexBufferManager& CRenderer::GetVertexBufferManager() +{ + return m->vertexBufferManager; +} + CShaderManager& CRenderer::GetShaderManager() { return m->shaderManager; } CTimeManager& CRenderer::GetTimeManager() { return m->timeManager; } CPostprocManager& CRenderer::GetPostprocManager() { return m->postprocManager; } CSceneRenderer& CRenderer::GetSceneRenderer() { return m->sceneRenderer; } CDebugRenderer& CRenderer::GetDebugRenderer() { return m->debugRenderer; } CFontManager& CRenderer::GetFontManager() { return m->fontManager; } void CRenderer::PreloadResourcesBeforeNextFrame() { m_ShouldPreloadResourcesBeforeNextFrame = true; } void CRenderer::MakeScreenShotOnNextFrame(ScreenShotType screenShotType) { m_ScreenShotType = screenShotType; } Renderer::Backend::IDeviceCommandContext* CRenderer::GetDeviceCommandContext() { return m->deviceCommandContext.get(); } Renderer::Backend::IVertexInputLayout* CRenderer::GetVertexInputLayout( const PS::span attributes) { const auto [it, inserted] = m->vertexInputLayouts.emplace( std::vector{attributes.begin(), attributes.end()}, nullptr); if (inserted) it->second = m->device->CreateVertexInputLayout(attributes); return it->second.get(); } Index: ps/trunk/source/renderer/Renderer.h =================================================================== --- ps/trunk/source/renderer/Renderer.h (revision 27906) +++ ps/trunk/source/renderer/Renderer.h (revision 27907) @@ -1,185 +1,188 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #ifndef INCLUDED_RENDERER #define INCLUDED_RENDERER #include "graphics/Camera.h" #include "graphics/ShaderDefines.h" #include "graphics/ShaderProgramPtr.h" #include "ps/containers/Span.h" #include "ps/Singleton.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/backend/IShaderProgram.h" #include "renderer/RenderingOptions.h" #include "renderer/Scene.h" #include class CDebugRenderer; class CFontManager; class CPostprocManager; class CSceneRenderer; class CShaderManager; class CTextureManager; class CTimeManager; +class CVertexBufferManager; #define g_Renderer CRenderer::GetSingleton() /** * Higher level interface on top of the whole frame rendering. It does know * what should be rendered and via which renderer but shouldn't know how to * render a particular area, like UI or scene. */ class CRenderer : public Singleton { public: // stats class - per frame counts of number of draw calls, poly counts etc struct Stats { // set all stats to zero void Reset() { memset(this, 0, sizeof(*this)); } // number of draw calls per frame - total DrawElements + Begin/End immediate mode loops size_t m_DrawCalls; // number of terrain triangles drawn size_t m_TerrainTris; // number of water triangles drawn size_t m_WaterTris; // number of (non-transparent) model triangles drawn size_t m_ModelTris; // number of overlay triangles drawn size_t m_OverlayTris; // number of splat passes for alphamapping size_t m_BlendSplats; // number of particles size_t m_Particles; }; enum class ScreenShotType { NONE, DEFAULT, BIG }; public: CRenderer(Renderer::Backend::IDevice* device); ~CRenderer(); // open up the renderer: performs any necessary initialisation bool Open(int width, int height); // resize renderer view void Resize(int width, int height); // return view width int GetWidth() const { return m_Width; } // return view height int GetHeight() const { return m_Height; } void RenderFrame(bool needsPresent); // signal frame start void BeginFrame(); // signal frame end void EndFrame(); // trigger a reload of shaders (when parameters they depend on have changed) void MakeShadersDirty(); // return stats accumulated for current frame Stats& GetStats() { return m_Stats; } CTextureManager& GetTextureManager(); + CVertexBufferManager& GetVertexBufferManager(); + CShaderManager& GetShaderManager(); CFontManager& GetFontManager(); CTimeManager& GetTimeManager(); CPostprocManager& GetPostprocManager(); CSceneRenderer& GetSceneRenderer(); CDebugRenderer& GetDebugRenderer(); /** * Performs a complete frame without presenting to force loading all needed * resources. It's used for the first frame on a game start. * TODO: It might be better to preload resources without a complete frame * rendering. */ void PreloadResourcesBeforeNextFrame(); /** * Makes a screenshot on the next RenderFrame according of the given * screenshot type. */ void MakeScreenShotOnNextFrame(ScreenShotType screenShotType); Renderer::Backend::IDeviceCommandContext* GetDeviceCommandContext(); /** * Returns a cached vertex input layout. The renderer owns the layout to be * able to share it between different clients. As backend should have * as few different layouts as possible. * The function isn't cheap so it should be called as rarely as possible. * TODO: we need to make VertexArray less error prone by passing layout. */ Renderer::Backend::IVertexInputLayout* GetVertexInputLayout( const PS::span attributes); protected: friend class CPatchRData; friend class CDecalRData; friend class HWLightingModelRenderer; friend class ShaderModelVertexRenderer; friend class InstancingModelRenderer; friend class CRenderingOptions; bool ShouldRender() const; void RenderFrameImpl(const bool renderGUI, const bool renderLogger); void RenderFrame2D(const bool renderGUI, const bool renderLogger); void RenderScreenShot(const bool needsPresent); void RenderBigScreenShot(const bool needsPresent); // SetRenderPath: Select the preferred render path. // This may only be called before Open(), because the layout of vertex arrays and other // data may depend on the chosen render path. void SetRenderPath(RenderPath rp); void ReloadShaders(); // Private data that is not needed by inline functions. class Internals; std::unique_ptr m; // view width int m_Width = 0; // view height int m_Height = 0; // per-frame renderer stats Stats m_Stats; bool m_ShouldPreloadResourcesBeforeNextFrame = false; ScreenShotType m_ScreenShotType = ScreenShotType::NONE; }; #endif // INCLUDED_RENDERER Index: ps/trunk/source/renderer/TexturedLineRData.cpp =================================================================== --- ps/trunk/source/renderer/TexturedLineRData.cpp (revision 27906) +++ ps/trunk/source/renderer/TexturedLineRData.cpp (revision 27907) @@ -1,474 +1,474 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "TexturedLineRData.h" #include "graphics/ShaderProgram.h" #include "graphics/Terrain.h" #include "maths/Frustum.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "ps/CStrInternStatic.h" #include "renderer/OverlayRenderer.h" #include "renderer/Renderer.h" #include "simulation2/Simulation2.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpWaterManager.h" -/* Note: this implementation uses g_VBMan directly rather than access it through the nicer VertexArray interface, +/* Note: this implementation uses CVertexBufferManager directly rather than access it through the nicer VertexArray interface, * because it allows you to work with variable amounts of vertices and indices more easily. New code should prefer * to use VertexArray where possible, though. */ // static Renderer::Backend::IVertexInputLayout* CTexturedLineRData::GetVertexInputLayout() { const uint32_t stride = sizeof(CTexturedLineRData::SVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(CTexturedLineRData::SVertex, m_Position), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(CTexturedLineRData::SVertex, m_UV), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(CTexturedLineRData::SVertex, m_UV), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; return g_Renderer.GetVertexInputLayout(attributes); } void CTexturedLineRData::Render( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* vertexInputLayout, const SOverlayTexturedLine& line, Renderer::Backend::IShaderProgram* shader) { if (!m_VB || !m_VBIndices) return; // might have failed to allocate // -- render main line quad strip ---------------------- line.m_TextureBase->UploadBackendTextureIfNeeded(deviceCommandContext); line.m_TextureMask->UploadBackendTextureIfNeeded(deviceCommandContext); ENSURE(!m_VB->m_Owner->GetBuffer()->IsDynamic()); ENSURE(!m_VBIndices->m_Owner->GetBuffer()->IsDynamic()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), line.m_TextureBase->GetBackendTexture()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_maskTex), line.m_TextureMask->GetBackendTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_objectColor), line.m_Color.AsFloatArray()); deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBuffer(0, m_VB->m_Owner->GetBuffer(), 0); deviceCommandContext->SetIndexBuffer(m_VBIndices->m_Owner->GetBuffer()); deviceCommandContext->DrawIndexed(m_VBIndices->m_Index, m_VBIndices->m_Count, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris += m_VBIndices->m_Count/3; } void CTexturedLineRData::Update(const SOverlayTexturedLine& line) { m_VBIndices.Reset(); m_VB.Reset(); if (!line.m_SimContext) { debug_warn(L"[TexturedLineRData] No SimContext set for textured overlay line, cannot render (no terrain data)"); return; } float v = 0.f; std::vector vertices; std::vector indices; const size_t n = line.m_Coords.size(); // number of line points bool closed = line.m_Closed; ENSURE(n >= 2); // minimum needed to avoid errors (also minimum value to make sense, can't draw a line between 1 point) // In each iteration, p1 is the position of vertex i, p0 is i-1, p2 is i+1. // To avoid slightly expensive terrain computations we cycle these around and // recompute p2 at the end of each iteration. CVector3D p0; CVector3D p1(line.m_Coords[0].X, 0, line.m_Coords[0].Y); CVector3D p2(line.m_Coords[1].X, 0, line.m_Coords[1].Y); if (closed) // grab the ending point so as to close the loop p0 = CVector3D(line.m_Coords[n - 1].X, 0, line.m_Coords[n - 1].Y); else // we don't want to loop around and use the direction towards the other end of the line, so create an artificial p0 that // extends the p2 -> p1 direction, and use that point instead p0 = p1 + (p1 - p2); bool p1floating = false; bool p2floating = false; // Compute terrain heights, clamped to the water height (and remember whether // each point was floating on water, for normal computation later) // TODO: if we ever support more than one water level per map, recompute this per point CmpPtr cmpWaterManager(*line.m_SimContext, SYSTEM_ENTITY); float w = cmpWaterManager ? cmpWaterManager->GetExactWaterLevel(p0.X, p0.Z) : 0.f; const CTerrain& terrain = line.m_SimContext->GetTerrain(); p0.Y = terrain.GetExactGroundLevel(p0.X, p0.Z); if (p0.Y < w) p0.Y = w; p1.Y = terrain.GetExactGroundLevel(p1.X, p1.Z); if (p1.Y < w) { p1.Y = w; p1floating = true; } p2.Y = terrain.GetExactGroundLevel(p2.X, p2.Z); if (p2.Y < w) { p2.Y = w; p2floating = true; } for (size_t i = 0; i < n; ++i) { // For vertex i, compute bisector of lines (i-1)..(i) and (i)..(i+1) // perpendicular to terrain normal // Normal is vertical if on water, else computed from terrain CVector3D norm; if (p1floating) norm = CVector3D(0, 1, 0); else norm = terrain.CalcExactNormal(p1.X, p1.Z); CVector3D b = ((p1 - p0).Normalized() + (p2 - p1).Normalized()).Cross(norm); // Adjust bisector length to match the line thickness, along the line's width float l = b.Dot((p2 - p1).Normalized().Cross(norm)); if (fabs(l) > 0.000001f) // avoid unlikely divide-by-zero b *= line.m_Thickness / l; // Push vertices and indices for each quad in GL_TRIANGLES order. The two triangles of each quad are indexed using // the winding orders (BR, BL, TR) and (TR, BL, TL) (where BR is bottom-right of this iteration's quad, TR top-right etc). SVertex vertex1(p1 + b + norm * OverlayRenderer::OVERLAY_VOFFSET, CVector2D(0.f, v)); SVertex vertex2(p1 - b + norm * OverlayRenderer::OVERLAY_VOFFSET, CVector2D(1.f, v)); vertices.push_back(vertex1); vertices.push_back(vertex2); u16 vertexCount = static_cast(vertices.size()); u16 index1 = vertexCount - 2; // index of vertex1 in this iteration (TR of this quad) u16 index2 = vertexCount - 1; // index of the vertex2 in this iteration (TL of this quad) if (i == 0) { // initial two vertices to continue building triangles from (n must be >= 2 for this to work) indices.push_back(index1); indices.push_back(index2); } else { u16 index1Prev = vertexCount - 4; // index of the vertex1 in the previous iteration (BR of this quad) u16 index2Prev = vertexCount - 3; // index of the vertex2 in the previous iteration (BL of this quad) ENSURE(index1Prev < vertexCount); ENSURE(index2Prev < vertexCount); // Add two corner points from last iteration and join with one of our own corners to create triangle 1 // (don't need to do this if i == 1 because i == 0 are the first two ones, they don't need to be copied) if (i > 1) { indices.push_back(index1Prev); indices.push_back(index2Prev); } indices.push_back(index1); // complete triangle 1 // create triangle 2, specifying the adjacent side's vertices in the opposite order from triangle 1 indices.push_back(index1); indices.push_back(index2Prev); indices.push_back(index2); } // alternate V coordinate for debugging v = 1 - v; // cycle the p's and compute the new p2 p0 = p1; p1 = p2; p1floating = p2floating; // if in closed mode, wrap around the coordinate array for p2 -- otherwise, extend linearly if (!closed && i == n-2) // next iteration is the last point of the line, so create an artificial p2 that extends the p0 -> p1 direction p2 = p1 + (p1 - p0); else p2 = CVector3D(line.m_Coords[(i + 2) % n].X, 0, line.m_Coords[(i + 2) % n].Y); p2.Y = terrain.GetExactGroundLevel(p2.X, p2.Z); if (p2.Y < w) { p2.Y = w; p2floating = true; } else p2floating = false; } if (closed) { // close the path if (n % 2 == 0) { u16 vertexCount = static_cast(vertices.size()); indices.push_back(vertexCount - 2); indices.push_back(vertexCount - 1); indices.push_back(0); indices.push_back(0); indices.push_back(vertexCount - 1); indices.push_back(1); } else { // add two vertices to have the good UVs for the last quad SVertex vertex1(vertices[0].m_Position, CVector2D(0.f, 1.f)); SVertex vertex2(vertices[1].m_Position, CVector2D(1.f, 1.f)); vertices.push_back(vertex1); vertices.push_back(vertex2); u16 vertexCount = static_cast(vertices.size()); indices.push_back(vertexCount - 4); indices.push_back(vertexCount - 3); indices.push_back(vertexCount - 2); indices.push_back(vertexCount - 2); indices.push_back(vertexCount - 3); indices.push_back(vertexCount - 1); } } else { // Create start and end caps. On either end, this is done by taking the centroid between the last and second-to-last pair of // vertices that was generated along the path (i.e. the vertex1's and vertex2's from above), taking a directional vector // between them, and drawing the line cap in the plane given by the two butt-end corner points plus said vector. std::vector capIndices; std::vector capVertices; // create end cap CreateLineCap( line, // the order of these vertices is important here, swapping them produces caps at the wrong side vertices[vertices.size()-2].m_Position, // top-right vertex of last quad vertices[vertices.size()-1].m_Position, // top-left vertex of last quad // directional vector between centroids of last vertex pair and second-to-last vertex pair (Centroid(vertices[vertices.size()-2], vertices[vertices.size()-1]) - Centroid(vertices[vertices.size()-4], vertices[vertices.size()-3])).Normalized(), line.m_EndCapType, capVertices, capIndices ); for (unsigned i = 0; i < capIndices.size(); i++) capIndices[i] += static_cast(vertices.size()); vertices.insert(vertices.end(), capVertices.begin(), capVertices.end()); indices.insert(indices.end(), capIndices.begin(), capIndices.end()); capIndices.clear(); capVertices.clear(); // create start cap CreateLineCap( line, // the order of these vertices is important here, swapping them produces caps at the wrong side vertices[1].m_Position, vertices[0].m_Position, // directional vector between centroids of first vertex pair and second vertex pair (Centroid(vertices[1], vertices[0]) - Centroid(vertices[3], vertices[2])).Normalized(), line.m_StartCapType, capVertices, capIndices ); for (unsigned i = 0; i < capIndices.size(); i++) capIndices[i] += static_cast(vertices.size()); vertices.insert(vertices.end(), capVertices.begin(), capVertices.end()); indices.insert(indices.end(), capIndices.begin(), capIndices.end()); } if (vertices.empty() || indices.empty()) return; // Indices for triangles, so must be multiple of 3. ENSURE(indices.size() % 3 == 0); m_BoundingBox = CBoundingBoxAligned(); for (const SVertex& vertex : vertices) m_BoundingBox += vertex.m_Position; - m_VB = g_VBMan.AllocateChunk( + m_VB = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SVertex), vertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false); // Allocation might fail (e.g. due to too many vertices). if (m_VB) { // Copy data into backend buffer. m_VB->m_Owner->UpdateChunkVertices(m_VB.Get(), &vertices[0]); for (size_t k = 0; k < indices.size(); ++k) indices[k] += static_cast(m_VB->m_Index); - m_VBIndices = g_VBMan.AllocateChunk( + m_VBIndices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false); if (m_VBIndices) m_VBIndices->m_Owner->UpdateChunkVertices(m_VBIndices.Get(), &indices[0]); } } void CTexturedLineRData::CreateLineCap(const SOverlayTexturedLine& line, const CVector3D& corner1, const CVector3D& corner2, const CVector3D& lineDirectionNormal, SOverlayTexturedLine::LineCapType endCapType, std::vector& verticesOut, std::vector& indicesOut) { if (endCapType == SOverlayTexturedLine::LINECAP_FLAT) return; // no action needed, this is the default // When not in closed mode, we've created artificial points for the start- and endpoints that extend the line in the // direction of the first and the last segment, respectively. Thus, we know both the start and endpoints have perpendicular // butt endings, i.e. the end corner vertices on either side of the line extend perpendicularly from the segment direction. // That is to say, when viewed from the top, we will have something like // . // this: and not like this: /| // ----+ / | // | / . // | / // ----+ / // int roundCapPoints = 8; // amount of points to sample along the semicircle for rounded caps (including corner points) float radius = line.m_Thickness; CVector3D centerPoint = (corner1 + corner2) * 0.5f; SVertex centerVertex(centerPoint, CVector2D(0.5f, 0.5f)); u16 indexOffset = static_cast(verticesOut.size()); // index offset in verticesOut from where we start adding our vertices switch (endCapType) { case SOverlayTexturedLine::LINECAP_SHARP: { roundCapPoints = 3; // creates only one point directly ahead radius *= 1.5f; // make it a bit sharper (note that we don't use the radius for the butt-end corner points so it should be ok) centerVertex.m_UV.X = 0.480f; // slight visual correction to make the texture match up better at the corner points } FALLTHROUGH; case SOverlayTexturedLine::LINECAP_ROUND: { // Draw a rounded line cap in the 3D plane of the line specified by the two corner points and the normal vector of the // line's direction. The terrain normal at the centroid between the two corner points is perpendicular to this plane. // The way this works is by taking a vector from the corner points' centroid to one of the corner points (which is then // of radius length), and rotate it around the terrain normal vector in that centroid. This will rotate the vector in // the line's plane, producing the desired rounded cap. // To please OpenGL's winding order, this angle needs to be negated depending on whether we start rotating from // the (center -> corner1) or (center -> corner2) vector. For the (center -> corner2) vector, we apparently need to use // the negated angle. float stepAngle = -(float)(M_PI/(roundCapPoints-1)); // Push the vertices in triangle fan order (easy to generate GL_TRIANGLES indices for afterwards) // Note that we're manually adding the corner vertices instead of having them be generated by the rotating vector. // This is because we want to support an overly large radius to make the sharp line ending look sharper. verticesOut.push_back(centerVertex); verticesOut.push_back(SVertex(corner2, CVector2D())); // Get the base vector that we will incrementally rotate in the cap plane to produce the radial sample points. // Normally corner2 - centerPoint would suffice for this since it is of radius length, but we want to support custom // radii to support tuning the 'sharpness' of sharp end caps (see above) CVector3D rotationBaseVector = (corner2 - centerPoint).Normalized() * radius; // Calculate the normal vector of the plane in which we're going to be drawing the line cap. This is the vector that // is perpendicular to both baseVector and the 'lineDirectionNormal' vector indicating the direction of the line. // Note that we shouldn't use terrain->CalcExactNormal() here because if the line is being rendered on top of water, // then CalcExactNormal will return the normal vector of the terrain that's underwater (which can be quite funky). CVector3D capPlaneNormal = lineDirectionNormal.Cross(rotationBaseVector).Normalized(); for (int i = 1; i < roundCapPoints - 1; ++i) { // Rotate the centerPoint -> corner vector by i*stepAngle radians around the cap plane normal at the center point. CQuaternion quatRotation; quatRotation.FromAxisAngle(capPlaneNormal, i * stepAngle); CVector3D worldPos3D = centerPoint + quatRotation.Rotate(rotationBaseVector); // Let v range from 0 to 1 as we move along the semi-circle, keep u fixed at 0 (i.e. curve the left vertical edge // of the texture around the edge of the semicircle) float u = 0.f; float v = Clamp((i / static_cast(roundCapPoints - 1)), 0.f, 1.f); // pos, u, v verticesOut.push_back(SVertex(worldPos3D, CVector2D(u, v))); } // connect back to the other butt-end corner point to complete the semicircle verticesOut.push_back(SVertex(corner1, CVector2D(0.f, 1.f))); // now push indices in GL_TRIANGLES order; vertices[indexOffset] is the center vertex, vertices[indexOffset + 1] is the // first corner point, then a bunch of radial samples, and then at the end we have the other corner point again. So: for (int i=1; i < roundCapPoints; ++i) { indicesOut.push_back(indexOffset); // center vertex indicesOut.push_back(indexOffset + i); indicesOut.push_back(indexOffset + i + 1); } } break; case SOverlayTexturedLine::LINECAP_SQUARE: { // Extend the (corner1 -> corner2) vector along the direction normal and draw a square line ending consisting of // three triangles (sort of like a triangle fan) // NOTE: The order in which the vertices are pushed out determines the visibility, as they // are rendered only one-sided; the wrong order of vertices will make the cap visible only from the bottom. verticesOut.push_back(centerVertex); verticesOut.push_back(SVertex(corner2, CVector2D())); verticesOut.push_back(SVertex(corner2 + (lineDirectionNormal * (line.m_Thickness)), CVector2D(0.f, 0.33333f))); // extend butt corner point 2 along the normal vector verticesOut.push_back(SVertex(corner1 + (lineDirectionNormal * (line.m_Thickness)), CVector2D(0.f, 0.66666f))); // extend butt corner point 1 along the normal vector verticesOut.push_back(SVertex(corner1, CVector2D(0.f, 1.0f))); // push butt corner point 1 for (int i=1; i < 4; ++i) { indicesOut.push_back(indexOffset); // center point indicesOut.push_back(indexOffset + i); indicesOut.push_back(indexOffset + i + 1); } } break; default: break; } } bool CTexturedLineRData::IsVisibleInFrustum(const CFrustum& frustum) const { return frustum.IsBoxVisible(m_BoundingBox); } Index: ps/trunk/source/renderer/VertexArray.cpp =================================================================== --- ps/trunk/source/renderer/VertexArray.cpp (revision 27906) +++ ps/trunk/source/renderer/VertexArray.cpp (revision 27907) @@ -1,311 +1,312 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/alignment.h" #include "lib/sysdep/rtl.h" #include "maths/Vector3D.h" #include "maths/Vector4D.h" #include "ps/CLogger.h" #include "graphics/Color.h" #include "graphics/SColor.h" +#include "renderer/Renderer.h" #include "renderer/VertexArray.h" #include "renderer/VertexBuffer.h" #include "renderer/VertexBufferManager.h" namespace { uint32_t GetAttributeSize(const Renderer::Backend::Format format) { switch (format) { case Renderer::Backend::Format::R8G8B8A8_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R8G8B8A8_UINT: return sizeof(u8) * 4; case Renderer::Backend::Format::A8_UNORM: return sizeof(u8); case Renderer::Backend::Format::R16_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R16_UINT: FALLTHROUGH; case Renderer::Backend::Format::R16_SINT: return sizeof(u16); case Renderer::Backend::Format::R16G16_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R16G16_UINT: FALLTHROUGH; case Renderer::Backend::Format::R16G16_SINT: return sizeof(u16) * 2; case Renderer::Backend::Format::R32_SFLOAT: return sizeof(float); case Renderer::Backend::Format::R32G32_SFLOAT: return sizeof(float) * 2; case Renderer::Backend::Format::R32G32B32_SFLOAT: return sizeof(float) * 3; case Renderer::Backend::Format::R32G32B32A32_SFLOAT: return sizeof(float) * 4; default: break; }; return 0; } } // anonymous namespace VertexArray::VertexArray( const Renderer::Backend::IBuffer::Type type, const bool dynamic) : m_Type(type), m_Dynamic(dynamic) { m_NumberOfVertices = 0; m_BackingStore = 0; m_Stride = 0; } VertexArray::~VertexArray() { Free(); } // Free all resources on destruction or when a layout parameter changes void VertexArray::Free() { rtl_FreeAligned(m_BackingStore); m_BackingStore = 0; m_VB.Reset(); } // Set the number of vertices stored in the array void VertexArray::SetNumberOfVertices(const size_t numberOfVertices) { if (numberOfVertices == m_NumberOfVertices) return; Free(); m_NumberOfVertices = numberOfVertices; } // Add vertex attributes like Position, Normal, UV void VertexArray::AddAttribute(Attribute* attr) { // Attribute is supported is its size is greater than zero. ENSURE(GetAttributeSize(attr->format) > 0 && "Unsupported attribute."); attr->vertexArray = this; m_Attributes.push_back(attr); Free(); } // Template specialization for GetIterator(). // We can put this into the source file because only a fixed set of types // is supported for type safety. template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE( format == Renderer::Backend::Format::R32G32B32_SFLOAT || format == Renderer::Backend::Format::R32G32B32A32_SFLOAT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R32G32B32A32_SFLOAT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R32G32_SFLOAT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE( format == Renderer::Backend::Format::R8G8B8A8_UNORM || format == Renderer::Backend::Format::R8G8B8A8_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16G16_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::A8_UNORM); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE( format == Renderer::Backend::Format::R8G8B8A8_UNORM || format == Renderer::Backend::Format::R8G8B8A8_UINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16_SINT); return vertexArray->MakeIterator(this); } template<> VertexArrayIterator VertexArray::Attribute::GetIterator() const { ENSURE(vertexArray); ENSURE(format == Renderer::Backend::Format::R16G16_SINT); return vertexArray->MakeIterator(this); } static uint32_t RoundStride(uint32_t stride) { if (stride <= 0) return 0; if (stride <= 4) return 4; if (stride <= 8) return 8; if (stride <= 16) return 16; return Align<32>(stride); } // Re-layout by assigning offsets on a first-come first-serve basis, // then round up to a reasonable stride. // Backing store is also created here, backend buffers are created on upload. void VertexArray::Layout() { Free(); m_Stride = 0; for (ssize_t idx = m_Attributes.size()-1; idx >= 0; --idx) { Attribute* attr = m_Attributes[idx]; if (attr->format == Renderer::Backend::Format::UNDEFINED) continue; const uint32_t attrSize = GetAttributeSize(attr->format); ENSURE(attrSize > 0); attr->offset = m_Stride; m_Stride += attrSize; if (m_Type == Renderer::Backend::IBuffer::Type::VERTEX) m_Stride = Align<4>(m_Stride); } if (m_Type == Renderer::Backend::IBuffer::Type::VERTEX) m_Stride = RoundStride(m_Stride); if (m_Stride) m_BackingStore = (char*)rtl_AllocateAligned(m_Stride * m_NumberOfVertices, 16); } void VertexArray::PrepareForRendering() { m_VB->m_Owner->PrepareForRendering(m_VB.Get()); } // (Re-)Upload the attributes. // Create the backend buffer if necessary. void VertexArray::Upload() { ENSURE(m_BackingStore); if (!m_VB) { - m_VB = g_VBMan.AllocateChunk( + m_VB = g_Renderer.GetVertexBufferManager().AllocateChunk( m_Stride, m_NumberOfVertices, m_Type, m_Dynamic, m_BackingStore); } if (!m_VB) { LOGERROR("Failed to allocate backend buffer for vertex array"); return; } m_VB->m_Owner->UpdateChunkVertices(m_VB.Get(), m_BackingStore); } void VertexArray::UploadIfNeeded( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { m_VB->m_Owner->UploadIfNeeded(deviceCommandContext); } // Free the backing store to save some memory void VertexArray::FreeBackingStore() { // In streaming modes, the backing store must be retained ENSURE(!CVertexBuffer::UseStreaming(m_Dynamic)); rtl_FreeAligned(m_BackingStore); m_BackingStore = 0; } VertexIndexArray::VertexIndexArray(const bool dynamic) : VertexArray(Renderer::Backend::IBuffer::Type::INDEX, dynamic) { m_Attr.format = Renderer::Backend::Format::R16_UINT; AddAttribute(&m_Attr); } VertexArrayIterator VertexIndexArray::GetIterator() const { return m_Attr.GetIterator(); } Index: ps/trunk/source/renderer/VertexBuffer.cpp =================================================================== --- ps/trunk/source/renderer/VertexBuffer.cpp (revision 27906) +++ ps/trunk/source/renderer/VertexBuffer.cpp (revision 27907) @@ -1,324 +1,322 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "VertexBuffer.h" #include "lib/sysdep/cpu.h" #include "ps/CLogger.h" -#include "ps/Errors.h" -#include "ps/VideoMode.h" #include "renderer/backend/IDevice.h" #include "renderer/Renderer.h" #include #include #include // Absolute maximum (bytewise) size of each GL vertex buffer object. // Make it large enough for the maximum feasible mesh size (64K vertexes, // 64 bytes per vertex in InstancingModelRenderer). // TODO: measure what influence this has on performance constexpr std::size_t MAX_VB_SIZE_BYTES = 4 * 1024 * 1024; CVertexBuffer::CVertexBuffer( - const char* name, const size_t vertexSize, + Renderer::Backend::IDevice* device, const char* name, const size_t vertexSize, const Renderer::Backend::IBuffer::Type type, const bool dynamic) - : CVertexBuffer(name, vertexSize, type, dynamic, MAX_VB_SIZE_BYTES) + : CVertexBuffer(device, name, vertexSize, type, dynamic, MAX_VB_SIZE_BYTES) { } CVertexBuffer::CVertexBuffer( - const char* name, const size_t vertexSize, + Renderer::Backend::IDevice* device, const char* name, const size_t vertexSize, const Renderer::Backend::IBuffer::Type type, const bool dynamic, const size_t maximumBufferSize) : m_VertexSize(vertexSize), m_HasNeededChunks(false) { size_t size = maximumBufferSize; if (type == Renderer::Backend::IBuffer::Type::VERTEX) { // We want to store 16-bit indices to any vertex in a buffer, so the // buffer must never be bigger than vertexSize*64K bytes since we can // address at most 64K of them with 16-bit indices size = std::min(size, vertexSize * 65536); } else if (type == Renderer::Backend::IBuffer::Type::INDEX) { ENSURE(vertexSize == sizeof(u16)); } // store max/free vertex counts m_MaxVertices = m_FreeVertices = size / vertexSize; - m_Buffer = g_VideoMode.GetBackendDevice()->CreateBuffer( + m_Buffer = device->CreateBuffer( name, type, m_MaxVertices * m_VertexSize, dynamic); // create sole free chunk VBChunk* chunk = new VBChunk; chunk->m_Owner = this; chunk->m_Count = m_FreeVertices; chunk->m_Index = 0; m_FreeList.emplace_back(chunk); } CVertexBuffer::~CVertexBuffer() { // Must have released all chunks before destroying the buffer ENSURE(m_AllocList.empty()); m_Buffer.reset(); for (VBChunk* const& chunk : m_FreeList) delete chunk; } bool CVertexBuffer::CompatibleVertexType( const size_t vertexSize, const Renderer::Backend::IBuffer::Type type, const bool dynamic) const { ENSURE(m_Buffer); return type == m_Buffer->GetType() && dynamic == m_Buffer->IsDynamic() && vertexSize == m_VertexSize; } /////////////////////////////////////////////////////////////////////////////// // Allocate: try to allocate a buffer of given number of vertices (each of // given size), with the given type, and using the given texture - return null // if no free chunks available CVertexBuffer::VBChunk* CVertexBuffer::Allocate( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::IBuffer::Type type, const bool dynamic, void* backingStore) { // check this is the right kind of buffer if (!CompatibleVertexType(vertexSize, type, dynamic)) return nullptr; if (UseStreaming(dynamic)) ENSURE(backingStore != nullptr); // quick check there's enough vertices spare to allocate if (numberOfVertices > m_FreeVertices) return nullptr; // trawl free list looking for first free chunk with enough space std::vector::iterator best_iter = m_FreeList.end(); for (std::vector::iterator iter = m_FreeList.begin(); iter != m_FreeList.end(); ++iter) { if (numberOfVertices == (*iter)->m_Count) { best_iter = iter; break; } else if (numberOfVertices < (*iter)->m_Count && (best_iter == m_FreeList.end() || (*best_iter)->m_Count < (*iter)->m_Count)) best_iter = iter; } // We could not find a large enough chunk. if (best_iter == m_FreeList.end()) return nullptr; VBChunk* chunk = *best_iter; m_FreeList.erase(best_iter); m_FreeVertices -= chunk->m_Count; chunk->m_BackingStore = backingStore; chunk->m_Dirty = false; chunk->m_Needed = false; // split chunk into two; - allocate a new chunk using all unused vertices in the // found chunk, and add it to the free list if (chunk->m_Count > numberOfVertices) { VBChunk* newchunk = new VBChunk; newchunk->m_Owner = this; newchunk->m_Count = chunk->m_Count - numberOfVertices; newchunk->m_Index = chunk->m_Index + numberOfVertices; m_FreeList.emplace_back(newchunk); m_FreeVertices += newchunk->m_Count; // resize given chunk chunk->m_Count = numberOfVertices; } // return found chunk m_AllocList.push_back(chunk); return chunk; } /////////////////////////////////////////////////////////////////////////////// // Release: return given chunk to this buffer void CVertexBuffer::Release(VBChunk* chunk) { // Update total free count before potentially modifying this chunk's count m_FreeVertices += chunk->m_Count; m_AllocList.erase(std::find(m_AllocList.begin(), m_AllocList.end(), chunk)); // Sorting O(nlogn) shouldn't be too far from O(n) by performance, because // the container is partly sorted already. std::sort( m_FreeList.begin(), m_FreeList.end(), [](const VBChunk* chunk1, const VBChunk* chunk2) -> bool { return chunk1->m_Index < chunk2->m_Index; }); // Coalesce with any free-list items that are adjacent to this chunk; // merge the found chunk with the new one, and remove the old one // from the list. for (std::vector::iterator iter = m_FreeList.begin(); iter != m_FreeList.end();) { if ((*iter)->m_Index == chunk->m_Index + chunk->m_Count || (*iter)->m_Index + (*iter)->m_Count == chunk->m_Index) { chunk->m_Index = std::min(chunk->m_Index, (*iter)->m_Index); chunk->m_Count += (*iter)->m_Count; delete *iter; iter = m_FreeList.erase(iter); if (!m_FreeList.empty() && iter != m_FreeList.begin()) iter = std::prev(iter); } else { ++iter; } } m_FreeList.emplace_back(chunk); } /////////////////////////////////////////////////////////////////////////////// // UpdateChunkVertices: update vertex data for given chunk void CVertexBuffer::UpdateChunkVertices(VBChunk* chunk, void* data) { ENSURE(m_Buffer); if (UseStreaming(m_Buffer->IsDynamic())) { // The backend buffer is now out of sync with the backing store. chunk->m_Dirty = true; // Sanity check: Make sure the caller hasn't tried to reallocate // their backing store. ENSURE(data == chunk->m_BackingStore); } else { ENSURE(data); g_Renderer.GetDeviceCommandContext()->UploadBufferRegion( m_Buffer.get(), data, chunk->m_Index * m_VertexSize, chunk->m_Count * m_VertexSize); } } void CVertexBuffer::UploadIfNeeded( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (UseStreaming(m_Buffer->IsDynamic())) { if (!m_HasNeededChunks) return; // If any chunks are out of sync with the current backend buffer, and are // needed for rendering this frame, we'll need to re-upload the backend buffer. bool needUpload = false; for (VBChunk* const& chunk : m_AllocList) { if (chunk->m_Dirty && chunk->m_Needed) { needUpload = true; break; } } if (needUpload) { deviceCommandContext->UploadBuffer(m_Buffer.get(), [&](u8* mappedData) { #ifndef NDEBUG // To help detect bugs where PrepareForRendering() was not called, // force all not-needed data to 0, so things won't get rendered // with undefined (but possibly still correct-looking) data. memset(mappedData, 0, m_MaxVertices * m_VertexSize); #endif // Copy only the chunks we need. (This condition is helpful when // the backend buffer contains data for every unit in the world, // but only a handful are visible on screen and we don't need to // bother copying the rest.) for (VBChunk* const& chunk : m_AllocList) if (chunk->m_Needed) std::memcpy(mappedData + chunk->m_Index * m_VertexSize, chunk->m_BackingStore, chunk->m_Count * m_VertexSize); }); // Anything we just uploaded is clean; anything else is dirty // since the rest of the backend buffer content is now undefined for (VBChunk* const& chunk : m_AllocList) { if (chunk->m_Needed) { chunk->m_Dirty = false; chunk->m_Needed = false; } else chunk->m_Dirty = true; } } else { // Reset the flags for the next phase. for (VBChunk* const& chunk : m_AllocList) chunk->m_Needed = false; } m_HasNeededChunks = false; } } size_t CVertexBuffer::GetBytesReserved() const { return MAX_VB_SIZE_BYTES; } size_t CVertexBuffer::GetBytesAllocated() const { return (m_MaxVertices - m_FreeVertices) * m_VertexSize; } void CVertexBuffer::DumpStatus() const { debug_printf("freeverts = %d\n", static_cast(m_FreeVertices)); size_t maxSize = 0; for (VBChunk* const& chunk : m_FreeList) { debug_printf("free chunk %p: size=%d\n", static_cast(chunk), static_cast(chunk->m_Count)); maxSize = std::max(chunk->m_Count, maxSize); } debug_printf("max size = %d\n", static_cast(maxSize)); } bool CVertexBuffer::UseStreaming(const bool dynamic) { return dynamic; } void CVertexBuffer::PrepareForRendering(VBChunk* chunk) { chunk->m_Needed = true; m_HasNeededChunks = true; } Index: ps/trunk/source/renderer/VertexBuffer.h =================================================================== --- ps/trunk/source/renderer/VertexBuffer.h (revision 27906) +++ ps/trunk/source/renderer/VertexBuffer.h (revision 27907) @@ -1,163 +1,165 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ /* * Encapsulation of backend buffers with batching and sharing. */ #ifndef INCLUDED_VERTEXBUFFER #define INCLUDED_VERTEXBUFFER #include "renderer/backend/IBuffer.h" +#include "renderer/backend/IDevice.h" #include "renderer/backend/IDeviceCommandContext.h" #include #include /** * CVertexBuffer: encapsulation of backend buffers, also supplying * some additional functionality for sharing buffers between multiple objects. * * The class can be used in two modes, depending on the usage parameter: * * Static buffer: Call Allocate() with backingStore = nullptr. Then call * UpdateChunkVertices() with any pointer - the data will be immediately copied * to the buffer. This should be used for vertex data that rarely changes. * * Dynamic buffer: Call Allocate() with backingStore pointing * at some memory that will remain valid for the lifetime of the CVertexBuffer. * This should be used for vertex data that may change every frame. * Rendering is expected to occur in two phases: * - "Prepare" phase: * If this chunk is going to be used for rendering during the next rendering phase, * you must call PrepareForRendering(). * If the vertex data in backingStore has been modified since the last uploading phase, * you must call UpdateChunkVertices(). * - "Upload" phase: * UploadedIfNeeded() can be called (multiple times). The vertex data will be uploaded * to the GPU if necessary. * It is okay to have multiple prepare/upload cycles per frame (though slightly less * efficient), but they must occur sequentially. */ class CVertexBuffer { NONCOPYABLE(CVertexBuffer); public: // VBChunk: describes a portion of this vertex buffer struct VBChunk { // Owning (parent) vertex buffer CVertexBuffer* m_Owner; // Start index of this chunk in owner size_t m_Index; // Number of vertices used by chunk size_t m_Count; // If UseStreaming() is true, points at the data for this chunk void* m_BackingStore; // If true, the backend buffer is not consistent with the chunk's // backing store (and will need to be re-uploaded before rendering with // this chunk). bool m_Dirty; // If true, we have been told this chunk is going to be used for // rendering in the next uploading phase and will need to be uploaded bool m_Needed; private: // Only CVertexBuffer can construct/delete these - // (Other people should use g_VBMan.AllocateChunk) + // (Other people should use g_Renderer.GetVertexBufferManager().AllocateChunk) friend class CVertexBuffer; VBChunk() {} ~VBChunk() {} }; public: - // constructor, destructor CVertexBuffer( + Renderer::Backend::IDevice* device, const char* name, const size_t vertexSize, const Renderer::Backend::IBuffer::Type type, const bool dynamic); CVertexBuffer( + Renderer::Backend::IDevice* device, const char* name, const size_t vertexSize, const Renderer::Backend::IBuffer::Type type, const bool dynamic, const size_t maximumBufferSize); ~CVertexBuffer(); void UploadIfNeeded(Renderer::Backend::IDeviceCommandContext* deviceCommandContext); /// Make the vertex data available for the next usage. void PrepareForRendering(VBChunk* chunk); /// Update vertex data for given chunk. Transfers the provided data to the actual OpenGL vertex buffer. void UpdateChunkVertices(VBChunk* chunk, void* data); size_t GetVertexSize() const { return m_VertexSize; } size_t GetBytesReserved() const; size_t GetBytesAllocated() const; /// Returns true if this vertex buffer is compatible with the specified vertex type and intended usage. bool CompatibleVertexType( const size_t vertexSize, const Renderer::Backend::IBuffer::Type type, const bool dynamic) const; void DumpStatus() const; /** * Given the usage flags of a buffer that has been (or will be) allocated: * * If true, we assume the buffer is going to be modified on every frame, * so we will re-upload the entire buffer every frame using glMapBuffer. * This requires the buffer's owner to hold onto its backing store. * * If false, we assume it will change rarely, and use direct upload to * update it incrementally. The backing store can be freed to save memory. */ static bool UseStreaming(const bool dynamic); Renderer::Backend::IBuffer* GetBuffer() { return m_Buffer.get(); } private: friend class CVertexBufferManager; // allow allocate only via CVertexBufferManager /// Try to allocate a buffer of given number of vertices (each of given size), /// and with the given type - return null if no free chunks available VBChunk* Allocate( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::IBuffer::Type type, const bool dynamic, void* backingStore); /// Return given chunk to this buffer void Release(VBChunk* chunk); /// Vertex size of this vertex buffer size_t m_VertexSize; /// Number of vertices of above size in this buffer size_t m_MaxVertices; /// List of free chunks in this buffer std::vector m_FreeList; /// List of allocated chunks std::vector m_AllocList; /// Available free vertices - total of all free vertices in the free list size_t m_FreeVertices; std::unique_ptr m_Buffer; bool m_HasNeededChunks; }; #endif // INCLUDED_VERTEXBUFFER Index: ps/trunk/source/renderer/VertexBufferManager.cpp =================================================================== --- ps/trunk/source/renderer/VertexBufferManager.cpp (revision 27906) +++ ps/trunk/source/renderer/VertexBufferManager.cpp (revision 27907) @@ -1,207 +1,197 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "VertexBufferManager.h" #include "ps/CLogger.h" +#include "renderer/Renderer.h" #define DUMP_VB_STATS 0 // for debugging namespace { const char* GetBufferTypeName( const Renderer::Backend::IBuffer::Type type) { const char* name = "UnknownBuffer"; switch (type) { case Renderer::Backend::IBuffer::Type::VERTEX: name = "VertexBuffer"; break; case Renderer::Backend::IBuffer::Type::INDEX: name = "IndexBuffer"; break; default: debug_warn("Unknown buffer type"); break; } return name; } const char* GetGroupName( const CVertexBufferManager::Group group) { const char* name = "Unknown"; switch (group) { case CVertexBufferManager::Group::DEFAULT: name = "Default"; break; case CVertexBufferManager::Group::TERRAIN: name = "Terrain"; break; case CVertexBufferManager::Group::WATER: name = "Water"; break; default: debug_warn("Unknown buffer group"); break; } return name; } } // anonymous namespace -CVertexBufferManager g_VBMan; - CVertexBufferManager::Handle::Handle(Handle&& other) : m_Chunk(other.m_Chunk) { other.m_Chunk = nullptr; } CVertexBufferManager::Handle& CVertexBufferManager::Handle::operator=(Handle&& other) { if (&other == this) return *this; if (IsValid()) Reset(); Handle tmp(std::move(other)); swap(*this, tmp); return *this; } CVertexBufferManager::Handle::Handle(CVertexBuffer::VBChunk* chunk) : m_Chunk(chunk) { } void CVertexBufferManager::Handle::Reset() { if (!IsValid()) return; - g_VBMan.Release(m_Chunk); + g_Renderer.GetVertexBufferManager().Release(m_Chunk); m_Chunk = nullptr; } -// Explicit shutdown of the vertex buffer subsystem. -// This avoids the ordering issues that arise when using destructors of -// global instances. -void CVertexBufferManager::Shutdown() -{ - for (int group = static_cast(Group::DEFAULT); group < static_cast(Group::COUNT); ++group) - m_Buffers[group].clear(); -} - /** * AllocateChunk: try to allocate a buffer of given number of vertices (each of * given size), with the given type, and using the given texture - return null * if no free chunks available */ CVertexBufferManager::Handle CVertexBufferManager::AllocateChunk( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::IBuffer::Type type, const bool dynamic, void* backingStore, Group group) { ENSURE(vertexSize > 0); ENSURE(numberOfVertices > 0); CVertexBuffer::VBChunk* result = nullptr; if (CVertexBuffer::UseStreaming(dynamic)) ENSURE(backingStore != NULL); // TODO, RC - run some sanity checks on allocation request std::vector>& buffers = m_Buffers[static_cast(group)]; #if DUMP_VB_STATS debug_printf("\n============================\n# allocate vsize=%zu nverts=%zu\n\n", vertexSize, numVertices); for (const std::unique_ptr& buffer : buffers) { if (buffer->CompatibleVertexType(vertexSize, type, dynamic)) { debug_printf("%p\n", buffer.get()); buffer->DumpStatus(); } } #endif // iterate through all existing buffers testing for one that'll // satisfy the allocation for (const std::unique_ptr& buffer : buffers) { result = buffer->Allocate(vertexSize, numberOfVertices, type, dynamic, backingStore); if (result) return Handle(result); } char bufferName[64] = {0}; snprintf( bufferName, std::size(bufferName), "%s (%s, %zu%s)", GetBufferTypeName(type), GetGroupName(group), vertexSize, (dynamic ? ", dynamic" : "")); // got this far; need to allocate a new buffer buffers.emplace_back( group == Group::DEFAULT - ? std::make_unique(bufferName, vertexSize, type, dynamic) + ? std::make_unique(m_Device, bufferName, vertexSize, type, dynamic) // Reduces the buffer size for not so frequent buffers. - : std::make_unique(bufferName, vertexSize, type, dynamic, 1024 * 1024)); + : std::make_unique(m_Device, bufferName, vertexSize, type, dynamic, 1024 * 1024)); result = buffers.back()->Allocate(vertexSize, numberOfVertices, type, dynamic, backingStore); if (!result) { LOGERROR("Failed to create backend buffer (%zu*%zu)", vertexSize, numberOfVertices); return Handle(); } return Handle(result); } void CVertexBufferManager::Release(CVertexBuffer::VBChunk* chunk) { ENSURE(chunk); #if DUMP_VB_STATS debug_printf("\n============================\n# release %p nverts=%zu\n\n", chunk, chunk->m_Count); #endif chunk->m_Owner->Release(chunk); } size_t CVertexBufferManager::GetBytesReserved() const { size_t total = 0; for (int group = static_cast(Group::DEFAULT); group < static_cast(Group::COUNT); ++group) for (const std::unique_ptr& buffer : m_Buffers[static_cast(group)]) total += buffer->GetBytesReserved(); return total; } size_t CVertexBufferManager::GetBytesAllocated() const { size_t total = 0; for (int group = static_cast(Group::DEFAULT); group < static_cast(Group::COUNT); ++group) for (const std::unique_ptr& buffer : m_Buffers[static_cast(group)]) total += buffer->GetBytesAllocated(); return total; } Index: ps/trunk/source/renderer/VertexBufferManager.h =================================================================== --- ps/trunk/source/renderer/VertexBufferManager.h (revision 27906) +++ ps/trunk/source/renderer/VertexBufferManager.h (revision 27907) @@ -1,113 +1,111 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ /* * Allocate and destroy CVertexBuffers */ #ifndef INCLUDED_VERTEXBUFFERMANAGER #define INCLUDED_VERTEXBUFFERMANAGER #include "lib/types.h" #include "renderer/VertexBuffer.h" #include #include #include // CVertexBufferManager: owner object for CVertexBuffer objects; acts as // 'front end' for their allocation and destruction class CVertexBufferManager { public: + CVertexBufferManager(Renderer::Backend::IDevice* device) : m_Device(device) {} + enum class Group : u32 { DEFAULT, TERRAIN, WATER, COUNT }; // Helper for automatic VBChunk lifetime management. class Handle { public: Handle() = default; Handle(const Handle&) = delete; Handle& operator=(const Handle&) = delete; explicit Handle(CVertexBuffer::VBChunk* chunk); Handle(Handle&& other); Handle& operator=(Handle&& other); ~Handle() { Reset(); } bool IsValid() const { return m_Chunk != nullptr; } explicit operator bool() const { return IsValid(); } bool operator!() const { return !static_cast(*this); } void Reset(); friend void swap(Handle& lhs, Handle& rhs) { using std::swap; swap(lhs.m_Chunk, rhs.m_Chunk); } CVertexBuffer::VBChunk& operator*() const { return *m_Chunk; } CVertexBuffer::VBChunk* operator->() const { return m_Chunk; } CVertexBuffer::VBChunk* Get() const { return m_Chunk; } private: CVertexBuffer::VBChunk* m_Chunk = nullptr; }; /** * Try to allocate a vertex buffer of the given size and type. * * @param vertexSize size of each vertex in the buffer * @param numberOfVertices number of vertices in the buffer * @param type buffer type * @param dynamic will be buffer updated frequently or not * @param backingStore if not dynamic, this is nullptr; else for dynamic, * this must be a copy of the vertex data that remains valid for the * lifetime of the VBChunk * @return chunk, or empty handle if no free chunks available */ Handle AllocateChunk( const size_t vertexSize, const size_t numberOfVertices, const Renderer::Backend::IBuffer::Type type, const bool dynamic, void* backingStore = nullptr, Group group = Group::DEFAULT); /// Returns the given @p chunk to its owning buffer void Release(CVertexBuffer::VBChunk* chunk); size_t GetBytesReserved() const; size_t GetBytesAllocated() const; - /// Explicit shutdown of the vertex buffer subsystem; releases all currently-allocated buffers. - void Shutdown(); - private: + Renderer::Backend::IDevice* m_Device{nullptr}; /// List of all known vertex buffers std::vector> m_Buffers[static_cast(Group::COUNT)]; }; -extern CVertexBufferManager g_VBMan; - -#endif +#endif // INCLUDED_VERTEXBUFFERMANAGER Index: ps/trunk/source/renderer/WaterManager.cpp =================================================================== --- ps/trunk/source/renderer/WaterManager.cpp (revision 27906) +++ ps/trunk/source/renderer/WaterManager.cpp (revision 27907) @@ -1,1153 +1,1153 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgram.h" #include "lib/bits.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "maths/Vector2D.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/components/ICmpRangeManager.h" #include struct CoastalPoint { CoastalPoint(int idx, CVector2D pos) : index(idx), position(pos) {}; int index; CVector2D position; }; struct SWavesVertex { // vertex position CVector3D m_BasePosition; CVector3D m_ApexPosition; CVector3D m_SplashPosition; CVector3D m_RetreatPosition; CVector2D m_PerpVect; float m_UV[2]; }; cassert(sizeof(SWavesVertex) == 64); struct WaveObject { CVertexBufferManager::Handle m_VBVertices; CBoundingBoxAligned m_AABB; size_t m_Width; float m_TimeDiff; }; WaterManager::WaterManager(Renderer::Backend::IDevice* device) : m_Device(device) { // water m_RenderWater = false; // disabled until textures are successfully loaded m_WaterHeight = 5.0f; m_RefTextureSize = 0; m_WaterTexTimer = 0.0; m_WindAngle = 0.0f; m_Waviness = 8.0f; m_WaterColor = CColor(0.3f, 0.35f, 0.7f, 1.0f); m_WaterTint = CColor(0.28f, 0.3f, 0.59f, 1.0f); m_Murkiness = 0.45f; m_RepeatPeriod = 16.0f; m_WaterEffects = true; m_WaterFancyEffects = false; m_WaterRealDepth = false; m_WaterRefraction = false; m_WaterReflection = false; m_WaterType = L"ocean"; m_NeedsReloading = false; m_NeedInfoUpdate = true; m_MapSize = 0; m_updatei0 = 0; m_updatej0 = 0; m_updatei1 = 0; m_updatej1 = 0; } WaterManager::~WaterManager() { // Cleanup if the caller messed up UnloadWaterTextures(); m_ShoreWaves.clear(); m_ShoreWavesVBIndices.Reset(); m_DistanceHeightmap.reset(); m_WindStrength.reset(); m_FancyEffectsFramebuffer.reset(); m_FancyEffectsOccludersFramebuffer.reset(); m_RefractionFramebuffer.reset(); m_ReflectionFramebuffer.reset(); m_FancyTexture.reset(); m_FancyTextureDepth.reset(); m_ReflFboDepthTexture.reset(); m_RefrFboDepthTexture.reset(); } void WaterManager::Initialize() { const uint32_t stride = sizeof(SWavesVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_BasePosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::NORMAL, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SWavesVertex, m_PerpVect), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SWavesVertex, m_UV), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_ApexPosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV2, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_SplashPosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV3, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_RetreatPosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; m_ShoreVertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); } /////////////////////////////////////////////////////////////////// // Progressive load of water textures int WaterManager::LoadWaterTextures() { // TODO: this doesn't need to be progressive-loading any more // (since texture loading is async now) wchar_t pathname[PATH_MAX]; // Load diffuse grayscale images (for non-fancy water) for (size_t i = 0; i < ARRAY_SIZE(m_WaterTexture); ++i) { swprintf_s(pathname, ARRAY_SIZE(pathname), L"art/textures/animated/water/default/diffuse%02d.dds", (int)i+1); CTextureProperties textureProps(pathname); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_WaterTexture[i] = texture; } m_RenderWater = true; // Load normalmaps (for fancy water) ReloadWaterNormalTextures(); // Load CoastalWaves { CTextureProperties textureProps(L"art/textures/terrain/types/water/coastalWave.png"); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_WaveTex = texture; } // Load Foam { CTextureProperties textureProps(L"art/textures/terrain/types/water/foam.png"); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_FoamTex = texture; } RecreateOrLoadTexturesIfNeeded(); return 0; } void WaterManager::RecreateOrLoadTexturesIfNeeded() { // Use screen-sized textures for minimum artifacts. const size_t newRefTextureSize = round_up_to_pow2(g_Renderer.GetHeight()); if (m_RefTextureSize != newRefTextureSize) { m_ReflectionFramebuffer.reset(); m_ReflectionTexture.reset(); m_ReflFboDepthTexture.reset(); m_RefractionFramebuffer.reset(); m_RefractionTexture.reset(); m_RefrFboDepthTexture.reset(); m_RefTextureSize = newRefTextureSize; } const Renderer::Backend::Format depthFormat = m_Device->GetPreferredDepthStencilFormat( Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, true, false); // Create reflection textures. const bool needsReflectionTextures = g_RenderingOptions.GetWaterEffects() && g_RenderingOptions.GetWaterReflection(); if (needsReflectionTextures && !m_ReflectionTexture) { m_ReflectionTexture = m_Device->CreateTexture2D("WaterReflectionTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT)); m_ReflFboDepthTexture = m_Device->CreateTexture2D("WaterReflectionDepthTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, depthFormat, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::NEAREST, Renderer::Backend::Sampler::AddressMode::REPEAT)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_ReflectionTexture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.5f, 0.5f, 1.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_ReflFboDepthTexture.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_ReflectionFramebuffer = m_Device->CreateFramebuffer("ReflectionFramebuffer", &colorAttachment, &depthStencilAttachment); if (!m_ReflectionFramebuffer) { g_RenderingOptions.SetWaterReflection(false); UpdateQuality(); } } // Create refraction textures. const bool needsRefractionTextures = g_RenderingOptions.GetWaterEffects() && g_RenderingOptions.GetWaterRefraction(); if (needsRefractionTextures && !m_RefractionTexture) { m_RefractionTexture = m_Device->CreateTexture2D("WaterRefractionTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT)); m_RefrFboDepthTexture = m_Device->CreateTexture2D("WaterRefractionDepthTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, depthFormat, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::NEAREST, Renderer::Backend::Sampler::AddressMode::REPEAT)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_RefractionTexture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{1.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_RefrFboDepthTexture.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_RefractionFramebuffer = m_Device->CreateFramebuffer("RefractionFramebuffer", &colorAttachment, &depthStencilAttachment); if (!m_RefractionFramebuffer) { g_RenderingOptions.SetWaterRefraction(false); UpdateQuality(); } } const uint32_t newWidth = static_cast(g_Renderer.GetWidth()); const uint32_t newHeight = static_cast(g_Renderer.GetHeight()); if (m_FancyTexture && (m_FancyTexture->GetWidth() != newWidth || m_FancyTexture->GetHeight() != newHeight)) { m_FancyEffectsFramebuffer.reset(); m_FancyEffectsOccludersFramebuffer.reset(); m_FancyTexture.reset(); m_FancyTextureDepth.reset(); } // Create the Fancy Effects textures. const bool needsFancyTextures = g_RenderingOptions.GetWaterEffects() && g_RenderingOptions.GetWaterFancyEffects(); if (needsFancyTextures && !m_FancyTexture) { m_FancyTexture = m_Device->CreateTexture2D("WaterFancyTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, g_Renderer.GetWidth(), g_Renderer.GetHeight(), Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::REPEAT)); m_FancyTextureDepth = m_Device->CreateTexture2D("WaterFancyDepthTexture", Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, depthFormat, g_Renderer.GetWidth(), g_Renderer.GetHeight(), Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::REPEAT)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_FancyTexture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_FancyTextureDepth.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; // We need to store depth for later rendering occluders. depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_FancyEffectsFramebuffer = m_Device->CreateFramebuffer("FancyEffectsFramebuffer", &colorAttachment, &depthStencilAttachment); Renderer::Backend::SColorAttachment occludersColorAttachment{}; occludersColorAttachment.texture = m_FancyTexture.get(); occludersColorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; occludersColorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; occludersColorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment occludersDepthStencilAttachment{}; occludersDepthStencilAttachment.texture = m_FancyTextureDepth.get(); occludersDepthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; occludersDepthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::DONT_CARE; m_FancyEffectsOccludersFramebuffer = m_Device->CreateFramebuffer("FancyEffectsOccludersFramebuffer", &occludersColorAttachment, &occludersDepthStencilAttachment); if (!m_FancyEffectsFramebuffer || !m_FancyEffectsOccludersFramebuffer) { g_RenderingOptions.SetWaterRefraction(false); UpdateQuality(); } } } void WaterManager::ReloadWaterNormalTextures() { wchar_t pathname[PATH_MAX]; for (size_t i = 0; i < ARRAY_SIZE(m_NormalMap); ++i) { swprintf_s(pathname, ARRAY_SIZE(pathname), L"art/textures/animated/water/%ls/normal00%02d.png", m_WaterType.c_str(), static_cast(i) + 1); CTextureProperties textureProps(pathname); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); textureProps.SetAnisotropicFilter(true); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_NormalMap[i] = texture; } } /////////////////////////////////////////////////////////////////// // Unload water textures void WaterManager::UnloadWaterTextures() { for (size_t i = 0; i < ARRAY_SIZE(m_WaterTexture); i++) m_WaterTexture[i].reset(); for (size_t i = 0; i < ARRAY_SIZE(m_NormalMap); i++) m_NormalMap[i].reset(); m_RefractionFramebuffer.reset(); m_ReflectionFramebuffer.reset(); m_ReflectionTexture.reset(); m_RefractionTexture.reset(); } template static inline void ComputeDirection(float* distanceMap, const u16* heightmap, float waterHeight, size_t SideSize, size_t maxLevel) { #define ABOVEWATER(x, z) (HEIGHT_SCALE * heightmap[z*SideSize + x] >= waterHeight) #define UPDATELOOKAHEAD \ for (; lookahead <= id2+maxLevel && lookahead < SideSize && \ ((!Transpose && !ABOVEWATER(lookahead, id1)) || (Transpose && !ABOVEWATER(id1, lookahead))); ++lookahead) // Algorithm: // We want to know the distance to the closest shore point. Go through each line/column, // keep track of when we encountered the last shore point and how far ahead the next one is. for (size_t id1 = 0; id1 < SideSize; ++id1) { size_t id2 = 0; const size_t& x = Transpose ? id1 : id2; const size_t& z = Transpose ? id2 : id1; size_t level = ABOVEWATER(x, z) ? 0 : maxLevel; size_t lookahead = (size_t)(level > 0); UPDATELOOKAHEAD; // start moving for (; id2 < SideSize; ++id2) { // update current level if (ABOVEWATER(x, z)) level = 0; else level = std::min(level+1, maxLevel); // move lookahead if (lookahead == id2) ++lookahead; UPDATELOOKAHEAD; // This is the important bit: set the distance to either: // - the distance to the previous shore point (level) // - the distance to the next shore point (lookahead-id2) distanceMap[z*SideSize + x] = std::min(distanceMap[z*SideSize + x], (float)std::min(lookahead-id2, level)); } } #undef ABOVEWATER #undef UPDATELOOKAHEAD } /////////////////////////////////////////////////////////////////// // Calculate our binary heightmap from the terrain heightmap. void WaterManager::RecomputeDistanceHeightmap() { const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); if (!terrain.GetHeightMap()) return; size_t SideSize = m_MapSize; // we want to look ahead some distance, but not too much (less efficient and not interesting). This is our lookahead. const size_t maxLevel = 5; if (!m_DistanceHeightmap) { m_DistanceHeightmap = std::make_unique(SideSize * SideSize); std::fill(m_DistanceHeightmap.get(), m_DistanceHeightmap.get() + SideSize * SideSize, static_cast(maxLevel)); } // Create a manhattan-distance heightmap. // This could be refined to only be done near the coast itself, but it's probably not necessary. const u16* const heightmap = terrain.GetHeightMap(); ComputeDirection(m_DistanceHeightmap.get(), heightmap, m_WaterHeight, SideSize, maxLevel); ComputeDirection(m_DistanceHeightmap.get(), heightmap, m_WaterHeight, SideSize, maxLevel); } // This requires m_DistanceHeightmap to be defined properly. void WaterManager::CreateWaveMeshes() { if (m_MapSize == 0) return; const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); if (!terrain.GetHeightMap()) return; m_ShoreWaves.clear(); m_ShoreWavesVBIndices.Reset(); if (m_Waviness < 5.0f && m_WaterType != L"ocean") return; size_t SideSize = m_MapSize; // First step: get the points near the coast. std::set CoastalPointsSet; for (size_t z = 1; z < SideSize-1; ++z) for (size_t x = 1; x < SideSize-1; ++x) // get the points not on the shore but near it, ocean-side if (m_DistanceHeightmap[z*m_MapSize + x] > 0.5f && m_DistanceHeightmap[z*m_MapSize + x] < 1.5f) CoastalPointsSet.insert((z)*SideSize + x); // Second step: create chains out of those coastal points. static const int around[8][2] = { { -1,-1 }, { -1,0 }, { -1,1 }, { 0,1 }, { 1,1 }, { 1,0 }, { 1,-1 }, { 0,-1 } }; std::vector > CoastalPointsChains; while (!CoastalPointsSet.empty()) { int index = *(CoastalPointsSet.begin()); int x = index % SideSize; int y = (index - x ) / SideSize; std::deque Chain; Chain.push_front(CoastalPoint(index,CVector2D(x*4,y*4))); // Erase us. CoastalPointsSet.erase(CoastalPointsSet.begin()); // We're our starter points. At most we can have 2 points close to us. // We'll pick the first one and look for its neighbors (he can only have one new) // Up until we either reach the end of the chain, or ourselves. // Then go down the other direction if there is any. int neighbours[2] = { -1, -1 }; int nbNeighb = 0; for (int i = 0; i < 8; ++i) { if (CoastalPointsSet.count(x + around[i][0] + (y + around[i][1])*SideSize)) { if (nbNeighb < 2) neighbours[nbNeighb] = x + around[i][0] + (y + around[i][1])*SideSize; ++nbNeighb; } } if (nbNeighb > 2) continue; for (int i = 0; i < 2; ++i) { if (neighbours[i] == -1) continue; // Move to our neighboring point int xx = neighbours[i] % SideSize; int yy = (neighbours[i] - xx ) / SideSize; int indexx = xx + yy*SideSize; int endedChain = false; if (i == 0) Chain.push_back(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); else Chain.push_front(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); // If there's a loop we'll be the "other" neighboring point already so check for that. // We'll readd at the end/front the other one to have full squares. if (CoastalPointsSet.count(indexx) == 0) break; CoastalPointsSet.erase(indexx); // Start checking from there. while(!endedChain) { bool found = false; nbNeighb = 0; for (int p = 0; p < 8; ++p) { if (CoastalPointsSet.count(xx+around[p][0] + (yy + around[p][1])*SideSize)) { if (nbNeighb >= 2) { CoastalPointsSet.erase(xx + yy*SideSize); continue; } ++nbNeighb; // We've found a new point around us. // Move there xx = xx + around[p][0]; yy = yy + around[p][1]; indexx = xx + yy*SideSize; if (i == 0) Chain.push_back(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); else Chain.push_front(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); CoastalPointsSet.erase(xx + yy*SideSize); found = true; break; } } if (!found) endedChain = true; } } if (Chain.size() > 10) CoastalPointsChains.push_back(Chain); } // (optional) third step: Smooth chains out. // This is also really dumb. for (size_t i = 0; i < CoastalPointsChains.size(); ++i) { // Bump 1 for smoother. for (int p = 0; p < 3; ++p) { for (size_t j = 1; j < CoastalPointsChains[i].size()-1; ++j) { CVector2D realPos = CoastalPointsChains[i][j-1].position + CoastalPointsChains[i][j+1].position; CoastalPointsChains[i][j].position = (CoastalPointsChains[i][j].position + realPos/2.0f)/2.0f; } } } // Fourth step: create waves themselves, using those chains. We basically create subchains. u16 waveSizes = 14; // maximal size in width. // Construct indices buffer (we can afford one for all of them) std::vector water_indices; for (u16 a = 0; a < waveSizes - 1; ++a) { for (u16 rect = 0; rect < 7; ++rect) { water_indices.push_back(a * 9 + rect); water_indices.push_back(a * 9 + 9 + rect); water_indices.push_back(a * 9 + 1 + rect); water_indices.push_back(a * 9 + 9 + rect); water_indices.push_back(a * 9 + 10 + rect); water_indices.push_back(a * 9 + 1 + rect); } } // Generic indexes, max-length - m_ShoreWavesVBIndices = g_VBMan.AllocateChunk( + m_ShoreWavesVBIndices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(u16), water_indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false, nullptr, CVertexBufferManager::Group::WATER); m_ShoreWavesVBIndices->m_Owner->UpdateChunkVertices(m_ShoreWavesVBIndices.Get(), &water_indices[0]); float diff = (rand() % 50) / 5.0f; std::vector vertices, reversed; for (size_t i = 0; i < CoastalPointsChains.size(); ++i) { for (size_t j = 0; j < CoastalPointsChains[i].size()-waveSizes; ++j) { if (CoastalPointsChains[i].size()- 1 - j < waveSizes) break; u16 width = waveSizes; // First pass to get some parameters out. float outmost = 0.0f; // how far to move on the shore. float avgDepth = 0.0f; int sign = 1; CVector2D firstPerp(0,0), perp(0,0), lastPerp(0,0); for (u16 a = 0; a < waveSizes;++a) { lastPerp = perp; perp = CVector2D(0,0); int nb = 0; CVector2D pos = CoastalPointsChains[i][j+a].position; CVector2D posPlus; CVector2D posMinus; if (a > 0) { ++nb; posMinus = CoastalPointsChains[i][j+a-1].position; perp += pos-posMinus; } if (a < waveSizes-1) { ++nb; posPlus = CoastalPointsChains[i][j+a+1].position; perp += posPlus-pos; } perp /= nb; perp = CVector2D(-perp.Y,perp.X).Normalized(); if (a == 0) firstPerp = perp; if ( a > 1 && perp.Dot(lastPerp) < 0.90f && perp.Dot(firstPerp) < 0.70f) { width = a+1; break; } if (terrain.GetExactGroundLevel(pos.X+perp.X*1.5f, pos.Y+perp.Y*1.5f) > m_WaterHeight) sign = -1; avgDepth += terrain.GetExactGroundLevel(pos.X+sign*perp.X*20.0f, pos.Y+sign*perp.Y*20.0f) - m_WaterHeight; float localOutmost = -2.0f; while (localOutmost < 0.0f) { const float depth = terrain.GetExactGroundLevel( pos.X+sign*perp.X*localOutmost, pos.Y+sign*perp.Y*localOutmost) - m_WaterHeight; if (depth < 0.0f || depth > 0.6f) localOutmost += 0.2f; else break; } outmost += localOutmost; } if (width < 5) { j += 6; continue; } outmost /= width; if (outmost > -0.5f) { j += 3; continue; } outmost = -2.5f + outmost * m_Waviness/10.0f; avgDepth /= width; if (avgDepth > -1.3f) { j += 3; continue; } // we passed the checks, we can create a wave of size "width". std::unique_ptr shoreWave = std::make_unique(); vertices.clear(); vertices.reserve(9 * width); shoreWave->m_Width = width; shoreWave->m_TimeDiff = diff; diff += (rand() % 100) / 25.0f + 4.0f; for (u16 a = 0; a < width;++a) { perp = CVector2D(0,0); int nb = 0; CVector2D pos = CoastalPointsChains[i][j+a].position; CVector2D posPlus; CVector2D posMinus; if (a > 0) { ++nb; posMinus = CoastalPointsChains[i][j+a-1].position; perp += pos-posMinus; } if (a < waveSizes-1) { ++nb; posPlus = CoastalPointsChains[i][j+a+1].position; perp += posPlus-pos; } perp /= nb; perp = CVector2D(-perp.Y,perp.X).Normalized(); SWavesVertex point[9]; float baseHeight = 0.04f; float halfWidth = (width-1.0f)/2.0f; float sideNess = sqrtf(Clamp( (halfWidth - fabsf(a - halfWidth)) / 3.0f, 0.0f, 1.0f)); point[0].m_UV[0] = a; point[0].m_UV[1] = 8; point[1].m_UV[0] = a; point[1].m_UV[1] = 7; point[2].m_UV[0] = a; point[2].m_UV[1] = 6; point[3].m_UV[0] = a; point[3].m_UV[1] = 5; point[4].m_UV[0] = a; point[4].m_UV[1] = 4; point[5].m_UV[0] = a; point[5].m_UV[1] = 3; point[6].m_UV[0] = a; point[6].m_UV[1] = 2; point[7].m_UV[0] = a; point[7].m_UV[1] = 1; point[8].m_UV[0] = a; point[8].m_UV[1] = 0; point[0].m_PerpVect = perp; point[1].m_PerpVect = perp; point[2].m_PerpVect = perp; point[3].m_PerpVect = perp; point[4].m_PerpVect = perp; point[5].m_PerpVect = perp; point[6].m_PerpVect = perp; point[7].m_PerpVect = perp; point[8].m_PerpVect = perp; static const float perpT1[9] = { 6.0f, 6.05f, 6.1f, 6.2f, 6.3f, 6.4f, 6.5f, 6.6f, 9.7f }; static const float perpT2[9] = { 2.0f, 2.1f, 2.2f, 2.3f, 2.4f, 3.0f, 3.3f, 3.6f, 9.5f }; static const float perpT3[9] = { 1.1f, 0.7f, -0.2f, 0.0f, 0.6f, 1.3f, 2.2f, 3.6f, 9.0f }; static const float perpT4[9] = { 2.0f, 2.1f, 1.2f, 1.5f, 1.7f, 1.9f, 2.7f, 3.8f, 9.0f }; static const float heightT1[9] = { 0.0f, 0.2f, 0.5f, 0.8f, 0.9f, 0.85f, 0.6f, 0.2f, 0.0 }; static const float heightT2[9] = { -0.8f, -0.4f, 0.0f, 0.1f, 0.1f, 0.03f, 0.0f, 0.0f, 0.0 }; static const float heightT3[9] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0 }; for (size_t t = 0; t < 9; ++t) { const float terrHeight = 0.05f + terrain.GetExactGroundLevel( pos.X+sign*perp.X*(perpT1[t]+outmost), pos.Y+sign*perp.Y*(perpT1[t]+outmost)); point[t].m_BasePosition = CVector3D(pos.X+sign*perp.X*(perpT1[t]+outmost), baseHeight + heightT1[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT1[t]+outmost)); } for (size_t t = 0; t < 9; ++t) { const float terrHeight = 0.05f + terrain.GetExactGroundLevel( pos.X+sign*perp.X*(perpT2[t]+outmost), pos.Y+sign*perp.Y*(perpT2[t]+outmost)); point[t].m_ApexPosition = CVector3D(pos.X+sign*perp.X*(perpT2[t]+outmost), baseHeight + heightT1[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT2[t]+outmost)); } for (size_t t = 0; t < 9; ++t) { const float terrHeight = 0.05f + terrain.GetExactGroundLevel( pos.X+sign*perp.X*(perpT3[t]+outmost*sideNess), pos.Y+sign*perp.Y*(perpT3[t]+outmost*sideNess)); point[t].m_SplashPosition = CVector3D(pos.X+sign*perp.X*(perpT3[t]+outmost*sideNess), baseHeight + heightT2[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT3[t]+outmost*sideNess)); } for (size_t t = 0; t < 9; ++t) { const float terrHeight = 0.05f + terrain.GetExactGroundLevel( pos.X+sign*perp.X*(perpT4[t]+outmost), pos.Y+sign*perp.Y*(perpT4[t]+outmost)); point[t].m_RetreatPosition = CVector3D(pos.X+sign*perp.X*(perpT4[t]+outmost), baseHeight + heightT3[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT4[t]+outmost)); } vertices.push_back(point[8]); vertices.push_back(point[7]); vertices.push_back(point[6]); vertices.push_back(point[5]); vertices.push_back(point[4]); vertices.push_back(point[3]); vertices.push_back(point[2]); vertices.push_back(point[1]); vertices.push_back(point[0]); shoreWave->m_AABB += point[8].m_SplashPosition; shoreWave->m_AABB += point[8].m_BasePosition; shoreWave->m_AABB += point[0].m_SplashPosition; shoreWave->m_AABB += point[0].m_BasePosition; shoreWave->m_AABB += point[4].m_ApexPosition; } if (sign == 1) { // Let's do some fancy reversing. reversed.clear(); reversed.reserve(vertices.size()); for (int a = width - 1; a >= 0; --a) { for (size_t t = 0; t < 9; ++t) reversed.push_back(vertices[a * 9 + t]); } std::swap(vertices, reversed); } j += width/2-1; - shoreWave->m_VBVertices = g_VBMan.AllocateChunk( + shoreWave->m_VBVertices = g_Renderer.GetVertexBufferManager().AllocateChunk( sizeof(SWavesVertex), vertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::WATER); shoreWave->m_VBVertices->m_Owner->UpdateChunkVertices(shoreWave->m_VBVertices.Get(), &vertices[0]); m_ShoreWaves.emplace_back(std::move(shoreWave)); } } } void WaterManager::RenderWaves( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CFrustum& frustrum) { if (!m_WaterFancyEffects) return; m_WaveTex->UploadBackendTextureIfNeeded(deviceCommandContext); m_FoamTex->UploadBackendTextureIfNeeded(deviceCommandContext); GPU_SCOPED_LABEL(deviceCommandContext, "Render Waves"); Renderer::Backend::IFramebuffer* framebuffer = m_FancyEffectsFramebuffer.get(); deviceCommandContext->BeginFramebufferPass(framebuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = framebuffer->GetWidth(); viewportRect.height = framebuffer->GetHeight(); deviceCommandContext->SetViewports(1, &viewportRect); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_water_waves); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = tech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_waveTex), m_WaveTex->GetBackendTexture()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_foamTex), m_FoamTex->GetBackendTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_time), static_cast(m_WaterTexTimer)); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); for (size_t a = 0; a < m_ShoreWaves.size(); ++a) { if (!frustrum.IsBoxVisible(m_ShoreWaves[a]->m_AABB)) continue; CVertexBuffer::VBChunk* VBchunk = m_ShoreWaves[a]->m_VBVertices.Get(); ENSURE(!VBchunk->m_Owner->GetBuffer()->IsDynamic()); ENSURE(!m_ShoreWavesVBIndices->m_Owner->GetBuffer()->IsDynamic()); const uint32_t stride = sizeof(SWavesVertex); const uint32_t firstVertexOffset = VBchunk->m_Index * stride; deviceCommandContext->SetVertexInputLayout(m_ShoreVertexInputLayout); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_translation), m_ShoreWaves[a]->m_TimeDiff); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_width), static_cast(m_ShoreWaves[a]->m_Width)); deviceCommandContext->SetVertexBuffer( 0, VBchunk->m_Owner->GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_ShoreWavesVBIndices->m_Owner->GetBuffer()); const uint32_t indexCount = (m_ShoreWaves[a]->m_Width - 1) * (7 * 6); deviceCommandContext->DrawIndexed(m_ShoreWavesVBIndices->m_Index, indexCount, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_WaterTris += indexCount / 3; } deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); } void WaterManager::RecomputeWaterData() { if (!m_MapSize) return; RecomputeDistanceHeightmap(); RecomputeWindStrength(); CreateWaveMeshes(); } /////////////////////////////////////////////////////////////////// // Calculate the strength of the wind at a given point on the map. void WaterManager::RecomputeWindStrength() { if (m_MapSize <= 0) return; if (!m_WindStrength) m_WindStrength = std::make_unique(m_MapSize * m_MapSize); const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); if (!terrain.GetHeightMap()) return; CVector2D windDir = CVector2D(cos(m_WindAngle), sin(m_WindAngle)); int stepSize = 10; ssize_t windX = -round(stepSize * windDir.X); ssize_t windY = -round(stepSize * windDir.Y); struct SWindPoint { SWindPoint(size_t x, size_t y, float strength) : X(x), Y(y), windStrength(strength) {} ssize_t X; ssize_t Y; float windStrength; }; std::vector startingPoints; std::vector> movement; // Every increment, move each starting point by all of these. // Compute starting points (one or two edges of the map) and how much to move each computation increment. if (fabs(windDir.X) < 0.01f) { movement.emplace_back(0, windY > 0.f ? 1 : -1); startingPoints.reserve(m_MapSize); size_t start = windY > 0 ? 0 : m_MapSize - 1; for (size_t x = 0; x < m_MapSize; ++x) startingPoints.emplace_back(x, start, 0.f); } else if (fabs(windDir.Y) < 0.01f) { movement.emplace_back(windX > 0.f ? 1 : - 1, 0); startingPoints.reserve(m_MapSize); size_t start = windX > 0 ? 0 : m_MapSize - 1; for (size_t z = 0; z < m_MapSize; ++z) startingPoints.emplace_back(start, z, 0.f); } else { startingPoints.reserve(m_MapSize * 2); // Points along X. size_t start = windY > 0 ? 0 : m_MapSize - 1; for (size_t x = 0; x < m_MapSize; ++x) startingPoints.emplace_back(x, start, 0.f); // Points along Z, avoid repeating the corner point. start = windX > 0 ? 0 : m_MapSize - 1; if (windY > 0) for (size_t z = 1; z < m_MapSize; ++z) startingPoints.emplace_back(start, z, 0.f); else for (size_t z = 0; z < m_MapSize-1; ++z) startingPoints.emplace_back(start, z, 0.f); // Compute movement array. movement.reserve(std::max(std::abs(windX),std::abs(windY))); while (windX != 0 || windY != 0) { std::pair move = { windX == 0 ? 0 : windX > 0 ? +1 : -1, windY == 0 ? 0 : windY > 0 ? +1 : -1 }; windX -= move.first; windY -= move.second; movement.push_back(move); } } // We have all starting points ready, move them all until the map is covered. for (SWindPoint& point : startingPoints) { // Starting velocity is 1.0 unless in shallow water. m_WindStrength[point.Y * m_MapSize + point.X] = 1.f; const float depth = m_WaterHeight - terrain.GetVertexGroundLevel(point.X, point.Y); if (depth > 0.f && depth < 2.f) m_WindStrength[point.Y * m_MapSize + point.X] = depth / 2.f; point.windStrength = m_WindStrength[point.Y * m_MapSize + point.X]; bool onMap = true; while (onMap) for (size_t step = 0; step < movement.size(); ++step) { // Move wind speed towards the mean. point.windStrength = 0.15f + point.windStrength * 0.85f; // Adjust speed based on height difference, a positive height difference slowly increases speed (simulate venturi effect) // and a lower height reduces speed (wind protection from hills/...) const float heightDiff = std::max(m_WaterHeight, terrain.GetVertexGroundLevel( point.X + movement[step].first, point.Y + movement[step].second)) - std::max(m_WaterHeight, terrain.GetVertexGroundLevel(point.X, point.Y)); if (heightDiff > 0.f) point.windStrength = std::min(2.f, point.windStrength + std::min(4.f, heightDiff) / 40.f); else point.windStrength = std::max(0.f, point.windStrength + std::max(-4.f, heightDiff) / 5.f); point.X += movement[step].first; point.Y += movement[step].second; if (point.X < 0 || point.X >= static_cast(m_MapSize) || point.Y < 0 || point.Y >= static_cast(m_MapSize)) { onMap = false; break; } m_WindStrength[point.Y * m_MapSize + point.X] = point.windStrength; } } // TODO: should perhaps blur a little, or change the above code to incorporate neighboring tiles a bit. } //////////////////////////////////////////////////////////////////////// // TODO: This will always recalculate for now void WaterManager::SetMapSize(size_t size) { // TODO: Im' blindly trusting the user here. m_MapSize = size; m_NeedInfoUpdate = true; m_updatei0 = 0; m_updatei1 = size; m_updatej0 = 0; m_updatej1 = size; m_DistanceHeightmap.reset(); m_WindStrength.reset(); } //////////////////////////////////////////////////////////////////////// // This will set the bools properly void WaterManager::UpdateQuality() { if (g_RenderingOptions.GetWaterEffects() != m_WaterEffects) { m_WaterEffects = g_RenderingOptions.GetWaterEffects(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterFancyEffects() != m_WaterFancyEffects) { m_WaterFancyEffects = g_RenderingOptions.GetWaterFancyEffects(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterRealDepth() != m_WaterRealDepth) { m_WaterRealDepth = g_RenderingOptions.GetWaterRealDepth(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterRefraction() != m_WaterRefraction) { m_WaterRefraction = g_RenderingOptions.GetWaterRefraction(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterReflection() != m_WaterReflection) { m_WaterReflection = g_RenderingOptions.GetWaterReflection(); m_NeedsReloading = true; } } bool WaterManager::WillRenderFancyWater() const { return m_RenderWater && m_Device->GetBackend() != Renderer::Backend::Backend::GL_ARB && g_RenderingOptions.GetWaterEffects(); } size_t WaterManager::GetCurrentTextureIndex(const double& period) const { ENSURE(period > 0.0); return static_cast(m_WaterTexTimer * ARRAY_SIZE(m_WaterTexture) / period) % ARRAY_SIZE(m_WaterTexture); } size_t WaterManager::GetNextTextureIndex(const double& period) const { ENSURE(period > 0.0); return (GetCurrentTextureIndex(period) + 1) % ARRAY_SIZE(m_WaterTexture); }