diff --git a/Engine/Vfs/Test/world_layer_scan_tests.cpp b/Engine/Vfs/Test/world_layer_scan_tests.cpp index 0d43456..3d69f05 100644 --- a/Engine/Vfs/Test/world_layer_scan_tests.cpp +++ b/Engine/Vfs/Test/world_layer_scan_tests.cpp @@ -21,7 +21,8 @@ int main(int argc, char** argv) { const std::filesystem::path root = argv[1]; const auto catalog = world_layer_scan::scan(root); check(catalog.anchors.size() == 6, "one anchor per map"); - check(catalog.nodes.size() >= 200 && catalog.layers.size() == catalog.nodes.size(), "one row per layer"); + check(catalog.nodes.size() >= 200 && catalog.layers.size() < catalog.nodes.size() && + catalog.layers.size() + 8 > catalog.nodes.size(), "a row per layer but the essential ones"); for (std::size_t i = 0; i < catalog.nodes.size(); ++i) { const auto parent = catalog.nodes[i].parent; check(parent >= -1 && parent < static_cast(i), "parents come first"); @@ -31,6 +32,16 @@ int main(int argc, char** argv) { }; for (const auto* key : {"bam_tod_1_morning", "bam_tod_7_weathernight", "grom_tod_5_night", "bam_debug_tests"}) check(has(key), key); + // No row for what every map is built out of: a player cannot switch off + // the world and then wonder which row did it. + check(std::none_of(catalog.layers.begin(), catalog.layers.end(), [](const WorldLayer& layer) { + return layer.key.find("coregameassets") != std::string::npos; + }), "CoreGameAssets has no row to switch"); + check(std::any_of(catalog.nodes.begin(), catalog.nodes.end(), [](const WorldLayerNode& node) { + std::string bundle = node.bundle; + std::transform(bundle.begin(), bundle.end(), bundle.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return bundle.find("coregameassets") != std::string::npos; + }), "but it keeps its node, so its children are still reachable"); check(std::none_of(catalog.nodes.begin(), catalog.nodes.end(), [](const WorldLayerNode& node) { std::string bundle = node.bundle; std::transform(bundle.begin(), bundle.end(), bundle.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); diff --git a/Engine/Vfs/world_layer_scan.cpp b/Engine/Vfs/world_layer_scan.cpp index a019c1d..11ed8a4 100644 --- a/Engine/Vfs/world_layer_scan.cpp +++ b/Engine/Vfs/world_layer_scan.cpp @@ -78,6 +78,15 @@ std::vector read_references(const fs::path& gameRoot, const char* toc return result; } +// Layers there is no sense in switching off. They stay in the node tree, so +// their children are still found and still switchable, but they get no row of +// their own: CoreGameAssets holds what every map is built out of, and a player +// who turned it off lost the world with no clue which row had done it. A +// choice already stored for one is keyed by row, so it simply stops applying. +bool essential_layer(std::string_view bundle) { + return contains(leaf_of(lower(bundle)), "coregameassets"); +} + std::string category(std::string_view bundle) { const auto path = lower(bundle); const auto leaf = leaf_of(path); @@ -143,6 +152,7 @@ void add_map(WorldLayerCatalog& catalog, WorldMap map, const std::vector 1 ? slug(node.bundle) : slug(leaf)); auto label = leaf; @@ -154,7 +164,7 @@ void add_map(WorldLayerCatalog& catalog, WorldMap map, const std::vector #include @@ -221,6 +223,12 @@ Fonts load_fonts() { fonts.action = font(L"FONT_HEADING", L"seguibl.ttf", 50); fonts.title = font(L"FONT_BRUSH", L"seguibl.ttf", 86); io.FontDefault = fonts.body; + // Built here rather than on the first frame, so the log says whether it + // worked. A window with no atlas draws nothing but its clear colour. + const bool built = io.Fonts->Build(); + logging::log(built ? logging::Level::info : logging::Level::error, logging::Channel::launcher, + "Launcher fonts: atlas {}x{} {}.", io.Fonts->TexWidth, io.Fonts->TexHeight, + built ? "built" : "COULD NOT BE BUILT, so the window will be empty"); return fonts; } @@ -313,6 +321,9 @@ int run(const launcher_app::Session& session, const std::vector& a const int width = static_cast(S(design_width)); const int height = static_cast(S(design_height)); + logging::log(logging::Level::info, logging::Channel::launcher, + "Launcher window: {}x{} at scale {:.2f} (system DPI {}), work area {}x{}.", width, height, g_scale, + GetDpiForSystem(), work.right - work.left, work.bottom - work.top); const HWND window = CreateWindowExW(WS_EX_APPWINDOW, window_class.lpszClassName, L"ReSkate", WS_POPUP | WS_MINIMIZEBOX | WS_SYSMENU, work.left + (work.right - work.left - width) / 2, work.top + (work.bottom - work.top - height) / 2, width, height, nullptr, nullptr, instance, nullptr); @@ -334,7 +345,9 @@ int run(const launcher_app::Session& session, const std::vector& a ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); DestroyWindow(window); - throw std::runtime_error("Cannot start Direct3D 12 for the launcher window"); + throw std::runtime_error("This PC's graphics driver cannot draw the launcher's window. Update your " + "graphics driver and try again. If that does not help, the graphics card is below what ReSkate and " + "skate. need. The log names the card it tried to use."); } g_renderer = &renderer; { @@ -376,6 +389,7 @@ int run(const launcher_app::Session& session, const std::vector& a HANDLE game{}; DWORD game_id{}; bool hidden{}; + bool drew{}; while (running) { MSG message; while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { @@ -423,6 +437,10 @@ int run(const launcher_app::Session& session, const std::vector& a frame(launcher, fonts, window, ui, mods_panel); ImGui::Render(); renderer.render(); + if (!drew) { + drew = true; + logging::write(logging::Level::info, logging::Channel::launcher, "Launcher drew its first frame."); + } } ShowWindow(window, SW_HIDE); if (game) CloseHandle(game); diff --git a/Launcher/gui_renderer.cpp b/Launcher/gui_renderer.cpp index a43c31f..e3214cc 100644 --- a/Launcher/gui_renderer.cpp +++ b/Launcher/gui_renderer.cpp @@ -1,5 +1,9 @@ #include "gui_renderer.h" +#include "Engine/Core/Log/logging.h" + +#include + #include #include @@ -13,11 +17,52 @@ namespace dingosdk::launcher_gui::detail { bool Renderer::init(HWND window) { UINT factory_flags = 0; ComPtr factory; - if (FAILED(CreateDXGIFactory2(factory_flags, IID_PPV_ARGS(&factory)))) return false; - if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) { + if (FAILED(CreateDXGIFactory2(factory_flags, IID_PPV_ARGS(&factory)))) { + logging::write(logging::Level::error, logging::Channel::launcher, "Launcher renderer: no DXGI factory."); + return false; + } + bool software = false; + // Ask for the best card rather than whichever DXGI lists first. Where + // onboard graphics sit beside a real card, the default adapter is usually + // the onboard one, and its Direct3D 12 driver can be too old to draw the + // window at all while the card that would have worked goes unused. + if (ComPtr ranked; SUCCEEDED(factory.As(&ranked))) { + for (UINT index = 0; !device_; ++index) { + ComPtr candidate; + if (FAILED(ranked->EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, + IID_PPV_ARGS(&candidate)))) break; + DXGI_ADAPTER_DESC1 description{}; + // WARP is the last resort below, not a candidate here. + if (FAILED(candidate->GetDesc1(&description)) || (description.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)) + continue; + D3D12CreateDevice(candidate.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)); + } + } + if (!device_ && FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) { ComPtr warp; if (FAILED(factory->EnumWarpAdapter(IID_PPV_ARGS(&warp))) || - FAILED(D3D12CreateDevice(warp.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) return false; + FAILED(D3D12CreateDevice(warp.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) { + logging::write(logging::Level::error, logging::Channel::launcher, + "Launcher renderer: no Direct3D 12 device, not even the software one."); + return false; + } + software = true; + } + { + // Which card drew the window, so a blank one can be told apart from a + // driver problem, a software fallback or a remote session. + DXGI_ADAPTER_DESC1 description{}; + ComPtr adapter; + if (SUCCEEDED(factory->EnumAdapterByLuid(device_->GetAdapterLuid(), IID_PPV_ARGS(&adapter))) && + SUCCEEDED(adapter->GetDesc1(&description))) + logging::write(logging::Level::info, logging::Channel::launcher, + std::wstring(L"Launcher renderer: ") + description.Description + + (software ? L" (software fallback)" : L"") + L", " + + std::to_wstring(description.DedicatedVideoMemory / (1024 * 1024)) + L" MB dedicated"); + else + logging::write(logging::Level::warning, logging::Channel::launcher, + software ? "Launcher renderer: software fallback, adapter unidentified." + : "Launcher renderer: adapter unidentified."); } D3D12_DESCRIPTOR_HEAP_DESC rtv_desc{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, frame_count, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 1}; D3D12_DESCRIPTOR_HEAP_DESC srv_desc{D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 1 + max_textures, @@ -67,7 +112,22 @@ bool Renderer::init(HWND window) { info.SrvDescriptorHeap = srv_heap_.Get(); info.LegacySingleSrvCpuDescriptor = srv_heap_->GetCPUDescriptorHandleForHeapStart(); info.LegacySingleSrvGpuDescriptor = srv_heap_->GetGPUDescriptorHandleForHeapStart(); - return ImGui_ImplDX12_Init(&info); + if (!ImGui_ImplDX12_Init(&info)) { + logging::write(logging::Level::error, logging::Channel::launcher, + "Launcher renderer: the Direct3D 12 backend did not start."); + return false; + } + // The backend builds its pipeline and font texture on the first frame and + // ignores whether that worked, so a driver that cannot make one draws + // nothing at all and the window shows only its clear colour -- a black + // window, with a log that says everything succeeded. Build it here, where + // the answer can be acted on. + if (!ImGui_ImplDX12_CreateDeviceObjects()) { + logging::write(logging::Level::error, logging::Channel::launcher, + "Launcher renderer: this driver cannot create the Direct3D 12 pipeline the window draws with."); + return false; + } + return true; } void Renderer::render() {