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);
}