#include "../../engine/Shared.h" #include "../Global.h" #include "../Instance.h" #include "../Log.h" #include "../Mesh.h" #include "../Tools.h" #include "Rendering.h" #include "Dither.h" #include "SDL3/SDL_events.h" // IWYU pragma: keep #include "backends/imgui_impl_sdl3.h" #include "bgfx/defines.h" #include "bx/bx.h" #include "bx/debug.h" #include "bx/filepath.h" #include "bx/string.h" #include "bx/timer.h" #include #include #include #include #include #include #include #include "imgui-helper.h" #include "imgui-style.h" #include "imgui.h" using namespace std::chrono_literals; namespace Game { namespace { constexpr size_t ChunkSize = 1024; constexpr size_t MaxFileSize = ChunkSize * 1024 * 1024; constexpr size_t MaxChunkCount = MaxFileSize / ChunkSize; bool BufferedFileRead(FILE* const file, uint8_t* const writePtrIn, size_t& outTotalReadCount) { uint8_t* writePtr = writePtrIn; for (int32_t i = 0; i < MaxChunkCount; ++i) { size_t readCount = std::fread(writePtr, 1, ChunkSize, file); writePtr += readCount; outTotalReadCount += readCount; if (readCount != ChunkSize) { if (std::feof(file)) { return true; } int err = std::ferror(file); if (err != 0) { LOG_ERROR("Error reading file: %i", err); return false; } LOG_ERROR("This should never happen!"); return false; } } if (!std::feof(file)) { LOG_ERROR("File too big to be read!"); return false; } return true; } const bgfx::Memory* LoadBinaryFile(const char* path, int32_t retryCount = 1) { FILE* file = nullptr; for (int32_t i = 0; i < retryCount; ++i) { file = std::fopen(path, "rb"); if (file == nullptr) { if (i < retryCount - 1) { std::this_thread::sleep_for(100ms); LOG_WARN("Failed to open file, retrying..."); break; } else { LOG_ERROR("Failed to open file!"); return nullptr; } } } uint8_t* dataPtr = AllocateScratch(MaxFileSize); if (dataPtr == nullptr) { LOG_ERROR("Failed to load file, exceeded scratch memory! %s", path); return nullptr; } uint64_t totalReadCount = 0; bool success = BufferedFileRead(file, dataPtr, totalReadCount); std::fclose(file); if (!success) { LOG_ERROR("Failed to read file %s", path); ResizeLastScratchAlloc(0); return nullptr; } if (!ResizeLastScratchAlloc(totalReadCount)) { LOG_ERROR("This should never happen!"); return nullptr; } return bgfx::makeRef(dataPtr, totalReadCount); } const bgfx::Memory* loadFile(const char* path, bool appendZero = false, int32_t retryCount = 1) { FILE* file; for (int32_t i = 0; i < retryCount; ++i) { file = std::fopen(path, "rb"); if (file == nullptr && i < retryCount - 1) { std::this_thread::sleep_for(100ms); LOG_WARN("Failed to open file, retrying..."); break; } fseek(file, 0, SEEK_END); long fileSize = ftell(file); fseek(file, 0, SEEK_SET); long fileSizeX = appendZero ? (fileSize + 1) : fileSize; void* rawMem = AllocateScratch(fileSizeX); if (rawMem == nullptr) { LOG_ERROR("Failed to load file, exceeded scratch memory! %s", path); return nullptr; } const bgfx::Memory* mem = bgfx::makeRef(rawMem, fileSizeX); fread(mem->data, 1, fileSize, file); if (appendZero) { mem->data[mem->size - 1] = '\0'; } fclose(file); return mem; } LOG_WARN("File inaccessible! %s", path); return nullptr; } bgfx::ShaderHandle loadShader(const char* FILENAME) { const char* shaderPath = "???"; switch (bgfx::getRendererType()) { case bgfx::RendererType::Agc: case bgfx::RendererType::Nvn: case bgfx::RendererType::Count: case bgfx::RendererType::Noop: break; case bgfx::RendererType::Direct3D11: case bgfx::RendererType::Direct3D12: shaderPath = "game/compiled-shaders/dx11/"; break; case bgfx::RendererType::Gnm: shaderPath = "game/compiled-shaders/pssl/"; break; case bgfx::RendererType::Metal: shaderPath = "game/compiled-shaders/metal/"; break; case bgfx::RendererType::OpenGL: shaderPath = "game/compiled-shaders/glsl/"; break; case bgfx::RendererType::OpenGLES: shaderPath = "game/compiled-shaders/essl/"; break; case bgfx::RendererType::Vulkan: shaderPath = "game/compiled-shaders/spirv/"; break; } char buffer[512]{0}; bx::strCopy(buffer, sizeof(buffer), shaderPath); bx::strCat(buffer, sizeof(buffer), FILENAME); bx::strCat(buffer, sizeof(buffer), ".bin"); LOG("Loading shader at %s", buffer); const bgfx::Memory* mem = loadFile(buffer, true, 3); if (mem == nullptr) { LOG_WARN("Failed to load shader %s", FILENAME); return {}; } return bgfx::createShader(mem); } bgfx::TextureHandle loadTexture(const bx::FilePath& _filePath, uint64_t _flags, uint8_t _skip, bgfx::TextureInfo* _info, bimg::Orientation::Enum* _orientation) { BX_UNUSED(_skip); bgfx::TextureHandle handle = BGFX_INVALID_HANDLE; bx::Error err; const bgfx::Memory* data = LoadBinaryFile(_filePath.getCPtr()); if (data == nullptr || data->data == nullptr || data->size == 0) { LOG_WARN("Failed to find image %s", _filePath.getCPtr()); return handle; } bimg::ImageContainer imageContainer; if (!bimg::imageParse(imageContainer, data->data, data->size, &err)) { LOG_WARN("Failed to load image %s", _filePath.getCPtr()); return handle; } if (NULL != _orientation) { *_orientation = imageContainer.m_orientation; } // We're working with ktx textures, so the size should be in front of the actual data const uint32_t* texSizePtr = reinterpret_cast(data->data + imageContainer.m_offset); uint8_t* dataPtr = data->data + imageContainer.m_offset + sizeof(uint32_t); uint32_t dataSize = data->size - imageContainer.m_offset - sizeof(uint32_t); // Extra sanity check if (*texSizePtr != dataSize) { LOG_WARN("Texture size sanity check failed! %u != %u", texSizePtr, dataSize); return handle; } const bgfx::Memory* mem = bgfx::makeRef(dataPtr, dataSize); if (NULL != _info) { bgfx::calcTextureSize(*_info, uint16_t(imageContainer.m_width), uint16_t(imageContainer.m_height), uint16_t(imageContainer.m_depth), imageContainer.m_cubeMap, 1 < imageContainer.m_numMips, imageContainer.m_numLayers, bgfx::TextureFormat::Enum(imageContainer.m_format)); } if (imageContainer.m_cubeMap) { handle = bgfx::createTextureCube(uint16_t(imageContainer.m_width), 1 < imageContainer.m_numMips, imageContainer.m_numLayers, bgfx::TextureFormat::Enum(imageContainer.m_format), _flags, mem); } else if (1 < imageContainer.m_depth) { handle = bgfx::createTexture3D(uint16_t(imageContainer.m_width), uint16_t(imageContainer.m_height), uint16_t(imageContainer.m_depth), 1 < imageContainer.m_numMips, bgfx::TextureFormat::Enum(imageContainer.m_format), _flags, mem); } else if (bgfx::isTextureValid(0, false, imageContainer.m_numLayers, bgfx::TextureFormat::Enum(imageContainer.m_format), _flags)) { handle = bgfx::createTexture2D(uint16_t(imageContainer.m_width), uint16_t(imageContainer.m_height), 1 < imageContainer.m_numMips, imageContainer.m_numLayers, bgfx::TextureFormat::Enum(imageContainer.m_format), _flags, mem); } if (bgfx::isValid(handle)) { const bx::StringView name(_filePath); bgfx::setName(handle, name.getPtr(), name.getLength()); } return handle; } GameRendering* Instance = nullptr; } // namespace GameRendering& GameRendering::Get() { assert(Instance != nullptr); return *Instance; } void GameRendering::Setup(const RenderingSetup& setup) { LOG("--- RENDERING STARTUP ---"); ZoneScopedN("Setup"); SetupData = setup; if (Instance != nullptr) LOG_WARN("old rendering wasn't destroyed!"); Instance = this; SharedData& shared = GetShared(); bgfx::Init init; init.type = bgfx::RendererType::Direct3D12; #ifdef _DEBUG init.debug = true; // init.debug = false; #else init.debug = false; #endif init.platformData.nwh = shared.Window.Handle; init.platformData.ndt = nullptr; init.platformData.type = bgfx::NativeWindowHandleType::Default; init.resolution.width = shared.Window.WindowWidth; init.resolution.height = shared.Window.WindowHeight; init.resolution.reset = ResetFlags; LastWidth = shared.Window.WindowWidth; LastHeight = shared.Window.WindowHeight; LOG("%i by %i", init.resolution.width, init.resolution.height); if (!bgfx::init(init)) { LOG_ERROR("BGFX setup failed!"); } else { LOG("BGFX setup succeded!"); } bgfx::setViewClear(MainViewID, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x3399FFff, 1.0f, 0); bgfx::setViewRect(MainViewID, 0, 0, shared.Window.WindowWidth, shared.Window.WindowHeight); DefaultSampler = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler); LoadTextures(); LoadModels(Models, ModelCount); ReloadShaders(); if (SetupData.UseImgui) { imguiCreate(); SetImguiStyle(); if (!ImGui_ImplSDL3_InitForOther(shared.Window.SDLWindow)) { LOG_ERROR("Failed to set up imgui implementation!"); return; } // ImGui::GetIO().BackendFlags |= ImGuiBackendFlags_RendererHasViewports; ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; // ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // auto& platIO = ImGui::GetPlatformIO(); // platIO.Renderer_CreateWindow = TODO; // platIO.Platform_DestroyWindow = TODO; // platIO.Platform_SetWindowSize = TODO; // platIO.Platform_RenderWindow = TODO; } GameInstance& inst = GetInstance(); if (!inst.IsInitialized) { inst.Time.StartTime = bx::getHPCounter(); } if (SetupData.UseImgui) { if (inst.DebugData.ImguiIniSize > 0) { ImGui::LoadIniSettingsFromMemory(inst.DebugData.ImguiIni, inst.DebugData.ImguiIniSize); } else { ImGui::LoadIniSettingsFromDisk("imgui.ini"); } } DitherGen(DitherTextures, DitherRecursion); } void GameRendering::HandleEvents() { ZoneScopedN("Handle Events"); SharedData& shared = GetShared(); for (uint16_t i = 0; i < shared.Window.SDLEventCount; ++i) { if (SetupData.UseImgui) { ImGui_ImplSDL3_ProcessEvent(&shared.Window.SDLEvents[i]); } } shared.Window.SDLEventCount = 0; // Resize if necessary if (shared.Window.WindowWidth != LastWidth || shared.Window.WindowHeight != LastHeight) { bgfx::reset(shared.Window.WindowWidth, shared.Window.WindowHeight, ResetFlags); bgfx::setViewRect(MainViewID, 0, 0, shared.Window.WindowWidth, shared.Window.WindowHeight); LastWidth = shared.Window.WindowWidth; LastHeight = shared.Window.WindowHeight; } // Reload shaders if necessary FileChangeNotification* shaderChange = nullptr; if (shared.Dev.ChangedShaderCount > 0) { char buf[1024]{0}; for (int32_t i = 0; i < shared.Dev.ChangedShaderCount; ++i) { wcstombs(buf, shared.Dev.ChangedShaders[i].FileName, sizeof(buf)); LOG("Changed: %s", buf); } shared.Dev.ChangedShaderCount = 0; // TODO: when to destroy shader? // TODO: only reload changed shaders ReloadShaders(); } } void GameRendering::LoadTextures() { for (int32_t i = 0; i < MaxTextures; ++i) { Textures[i] = {}; } bx::Error err; bx::DirectoryReader reader{}; if (!reader.open("textures", &err) || !err.isOk()) { LOG_ERROR("Failed to read textures dir: %s", err.getMessage()); } bx::FileInfo info; int32_t textureFilePathCount = 0; bx::FilePath textureFilePaths[GameRendering::MaxTextures]; while (err.isOk()) { int32_t res = reader.read(&info, sizeof(info), &err); if (res == 0) break; // EOF if (res != sizeof(info)) { LOG_ERROR("Dir iter error: %s", err.getMessage()); break; } const bx::StringView ext = info.filePath.getExt(); bool isDDS = bx::strCmp(ext, ".dds") == 0; bool isKTX = bx::strCmp(ext, ".ktx") == 0; if ((isDDS || isKTX) && !ext.isEmpty() && info.type == bx::FileType::File) { if (textureFilePathCount >= GameRendering::MaxTextures) { LOG_ERROR("Texture limit reached!"); break; } textureFilePaths[textureFilePathCount] = info.filePath; textureFilePathCount++; } } LOG("Found %u textures!", textureFilePathCount); for (int32_t i = 0; i < textureFilePathCount; ++i) { bx::FilePath fullPath{"textures"}; fullPath.join(textureFilePaths[i]); Textures[i].RenderHandle = loadTexture(fullPath, BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE, 0, &Textures[i].Info, nullptr); Textures[i].SamplerHandle = DefaultSampler; Textures[i].TexHandle.TextureIdx = i; Textures[i].TexHandle.Asset.Idx = CrcPath(fullPath.getCPtr()); auto& debug = GetInstance().DebugData; if (debug.AssetCount < debug.MaxAssets) { debug.AssetHandles[debug.AssetCount] = Textures[i].TexHandle.Asset; bx::strCopy(debug.AssetHandlePaths[debug.AssetCount], sizeof(debug.AssetHandlePaths[debug.AssetCount]), fullPath.getCPtr()); ++debug.AssetCount; } } } void GameRendering::ReloadShaders() { Materials[Gen::EMaterial::Default] = Material::LoadFromShader("dither/vert", "dither/frag", MainViewID); Materials[Gen::EMaterial::UI] = Material::LoadFromShader("normal/vert", "normal/frag", MainViewID); } void GameRendering::Update() { ZoneScopedN("Rendering"); SharedData& shared = GetShared(); auto& time = GetInstance().Time; HandleEvents(); // Start Rendering if (SetupData.UseImgui) { ZoneScopedN("Imgui Start Frame"); imguiBeginFrame(20); ImGui_ImplSDL3_NewFrame(); ImGui::DockSpaceOverViewport(0, 0, ImGuiDockNodeFlags_PassthruCentralNode); } Tools::RenderDebugUI(*this); GetInstance().GameLevel.Update(); GetInstance().GameLevel.Render(MainViewID, Models, Materials, Textures); // Finish Frame if (SetupData.UseImgui) { ZoneScopedN("Imgui End Frame"); imguiEndFrame(); } if (SetupData.UseImgui && ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { ZoneScopedN("Imgui Platform Update"); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); } { ZoneScopedN("BGFX Frame"); START_PERF(); bgfx::frame(); END_PERF(shared.Window.PerfCounters, PerfCounterType::Submit, shared.Window.FrameCounter); } } void GameRendering::Shutdown() { ZoneScopedN("Shutdown"); LOG("--- RENDERING_SHUTDOWN ---"); for (int32_t i = 0; i < BX_COUNTOF(Textures); ++i) { if (isValid(Textures[i].RenderHandle)) { bgfx::destroy(Textures[i].RenderHandle); Textures[i].RenderHandle = {bgfx::kInvalidHandle}; } } for (int32_t i = 0; i < ModelCount; ++i) { bgfx::destroy(Models[i].VertexBuffer); bgfx::destroy(Models[i].IndexBuffer); } ModelCount = 0; CleanupDitherData(DitherTextures); if (SetupData.UseImgui) { ImGui::SaveIniSettingsToDisk("imgui.ini"); auto& debug = GetInstance().DebugData; const char* iniData = ImGui::SaveIniSettingsToMemory(reinterpret_cast(&debug.ImguiIniSize)); assert(debug.ImguiIniSize <= BX_COUNTOF(InstanceDebugData::ImguiIni)); bx::memCopy(debug.ImguiIni, iniData, bx::min(debug.ImguiIniSize, BX_COUNTOF(InstanceDebugData::ImguiIni))); ImGui_ImplSDL3_Shutdown(); imguiDestroy(); } bgfx::shutdown(); Instance = nullptr; } Material Material::LoadFromShader(const char* vertPath, const char* fragPath, uint16_t view) { BX_ASSERT(vertPath != nullptr && fragPath != nullptr, "Invalid shader path!"); bgfx::ShaderHandle vertexShader = loadShader(vertPath); bgfx::ShaderHandle fragmentShader = loadShader(fragPath); Material mat; mat.Shader = bgfx::createProgram(vertexShader, fragmentShader, true); mat.State = 0 | BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CCW | BGFX_STATE_MSAA; mat.Uniforms[Material::UTime] = bgfx::createUniform("u_time", bgfx::UniformType::Vec4); mat.Uniforms[Material::UDotColor] = bgfx::createUniform("u_dotColor", bgfx::UniformType::Vec4); mat.Uniforms[Material::UTexInfo] = bgfx::createUniform("u_texInfo", bgfx::UniformType::Vec4); mat.Uniforms[Material::UBaseColor] = bgfx::createUniform("u_baseColor", bgfx::UniformType::Vec4); mat.ViewID = view; return mat; } Gen::ModelHandle GameRendering::GetModelHandleFromPath(const char* path) { uint32_t AssetHandle = CrcPath(path); for (int32_t i = 0; i < ModelCount; ++i) { if (Models[i].Handle.Asset.Idx == AssetHandle) { return Models[i].Handle; } } LOG_WARN("Failed to find model for path %s", path); return {}; } } // namespace Game