diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b4a209..7a39030 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,13 @@ jobs: - name: Windows compiler environment if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 + - name: Enable the documented Windows long-path developer profile + if: runner.os == 'Windows' + shell: pwsh + run: | + $key = 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' + Write-Output "Previous LongPathsEnabled: $((Get-ItemProperty -Path $key).LongPathsEnabled)" + New-ItemProperty -Path $key -Name LongPathsEnabled -Value 1 -PropertyType DWORD -Force - name: Identify pinned Windows Vulkan SDK cache if: runner.os == 'Windows' id: vulkan-identity @@ -28,10 +35,8 @@ jobs: id: vulkan-sdk-cache uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 with: - key: ${{ steps.vulkan-identity.outputs.key }}-loader-only - restore-keys: | - ${{ steps.vulkan-identity.outputs.key }} - ${{ steps.vulkan-identity.outputs.legacy }} + key: ${{ steps.vulkan-identity.outputs.sdk_key }} + restore-keys: ${{ steps.vulkan-identity.outputs.sdk_legacy }} path: .cache/windows-graphics/sdk - name: Build Windows Vulkan headers and loader without a GPU driver if: runner.os == 'Windows' diff --git a/.github/workflows/windows-graphics.yml b/.github/workflows/windows-graphics.yml index 2685f8a..0dcfe90 100644 --- a/.github/workflows/windows-graphics.yml +++ b/.github/workflows/windows-graphics.yml @@ -21,6 +21,12 @@ jobs: uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 with: arch: x64 + - name: Enable the documented Windows long-path developer profile + shell: pwsh + run: | + $key = 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' + Write-Output "Previous LongPathsEnabled: $((Get-ItemProperty -Path $key).LongPathsEnabled)" + New-ItemProperty -Path $key -Name LongPathsEnabled -Value 1 -PropertyType DWORD -Force - name: Identify pinned Vulkan source cache id: vulkan-identity run: python tools/ci/prepare_windows_vulkan.py --cache-key diff --git a/CMakeLists.txt b/CMakeLists.txt index d164c34..6df4566 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,3 +107,5 @@ if(BUILD_TESTING) endif() include(cmake/Tutorials.cmake) + +include(cmake/WindowsPlatform.cmake) diff --git a/apps/editor_gui.cpp b/apps/editor_gui.cpp index e7ed465..998d76c 100644 --- a/apps/editor_gui.cpp +++ b/apps/editor_gui.cpp @@ -76,10 +76,10 @@ int run_editor_ui(Session& session, bool enable_mcp, std::uint64_t max_frames, auto encoded = png(renderer, region); const auto relative = arguments.value("path", std::string(".faset/screenshots/editor.png")); - require(std::filesystem::path(relative).extension() == ".png", "capture.path", + require(path_from_utf8(relative).extension() == ".png", "capture.path", "Editor screenshots must use a .png path"); atomic_write( - project_path(session.config().project_root, relative), + project_path(session.config().project_root, path_from_utf8(relative)), std::string_view(reinterpret_cast(encoded.data()), encoded.size())); return Json{{"path", relative}, {"mimeType", "image/png"}, diff --git a/apps/editor_main.cpp b/apps/editor_main.cpp index 2a91a31..bf8a9d6 100644 --- a/apps/editor_main.cpp +++ b/apps/editor_main.cpp @@ -47,11 +47,11 @@ void help() { "MCP uses JSON-RPC over stdio and only exposes authoring/editor services.\n"; } } // namespace -int main(int argc, char** argv) { +int editor_main(int argc, char** argv) { using namespace faset; using namespace faset::editor; try { - std::filesystem::path project, engine = FASET_ENGINE_SOURCE, scene, capture; + std::filesystem::path project, engine = path_from_utf8(FASET_ENGINE_SOURCE), scene, capture; std::string new_name, command; int dimension = 3; bool mcp = false, gui = true, explicit_gui = false, wait = false; @@ -67,15 +67,15 @@ int main(int argc, char** argv) { return 0; } if (arg == "--project") - project = value(); + project = path_from_utf8(value()); else if (arg == "--engine") - engine = value(); + engine = path_from_utf8(value()); else if (arg == "--new") new_name = value(); else if (arg == "--dimension") dimension = std::stoi(value()); else if (arg == "--scene") - scene = value(); + scene = path_from_utf8(value()); else if (arg == "--mcp") mcp = true; else if (arg == "--gui") { @@ -95,7 +95,7 @@ int main(int argc, char** argv) { frames = std::stoull(text); require(frames > 0 && frames <= 10000000, "cli.frames", "Frame count out of range"); } else if (arg == "--capture") - capture = value(); + capture = path_from_utf8(value()); else throw Error("cli.option", "Unknown option: " + arg); } @@ -130,7 +130,7 @@ int main(int argc, char** argv) { remember_project(project); #endif if (scene.empty()) - scene = settings.value("start_scene", std::string()); + scene = path_from_utf8(settings.value("start_scene", std::string())); if (!scene.empty() && std::filesystem::exists(project_path(session.config().project_root, scene))) session.authoring().open(scene); @@ -203,3 +203,13 @@ int main(int argc, char** argv) { return 1; } } + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return faset::run_utf8_main(argc, argv, editor_main); +} +#else +int main(int argc, char** argv) { + return editor_main(argc, argv); +} +#endif diff --git a/apps/player_main.cpp b/apps/player_main.cpp index 46d8bd5..5381df5 100644 --- a/apps/player_main.cpp +++ b/apps/player_main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -167,12 +168,13 @@ void validatePackagedShaders(const std::filesystem::path& directory) { for (const auto* extension : {".spv", ".reflection.json"}) { const auto path = shaders / (std::string(entry) + extension); if (!std::filesystem::is_regular_file(path)) - throw std::runtime_error("Packaged shader file is missing: " + path.string()); + throw std::runtime_error("Packaged shader file is missing: " + + faset::path_to_utf8(path)); } faset::render::validate_shader_bundle(shaders); } } // namespace -int main(int argc, char** argv) { +int player_main(int argc, char** argv) { const auto started = Clock::now(); try { std::filesystem::path scenePath, assetsPath, capturePath, controlPath, profilePath; @@ -207,15 +209,15 @@ int main(int argc, char** argv) { return argv[++i]; }; if (arg == "--scene") - scenePath = value(); + scenePath = faset::path_from_utf8(value()); else if (arg == "--assets") - assetsPath = value(); + assetsPath = faset::path_from_utf8(value()); else if (arg == "--capture") - capturePath = value(); + capturePath = faset::path_from_utf8(value()); else if (arg == "--control") - controlPath = value(); + controlPath = faset::path_from_utf8(value()); else if (arg == "--profile") - profilePath = value(); + profilePath = faset::path_from_utf8(value()); else if (arg == "--frames") maximumFrames = count(value()); else if (arg == "--headless") @@ -239,11 +241,12 @@ int main(int argc, char** argv) { assetsPath = scenePath.parent_path(); if (!std::filesystem::is_directory(assetsPath)) throw std::invalid_argument("Asset cache directory does not exist: " + - assetsPath.string()); + faset::path_to_utf8(assetsPath)); if (headless && maximumFrames == 0) maximumFrames = 1; const auto sceneReadStarted = Clock::now(); const auto document = faset::player::readScene(scenePath); + faset::runtime::validate_scene_schemas(document, faset::gameplay::schema()); const auto config = simulationConfig(document); const auto sceneReadFinished = Clock::now(); const auto executableRoot = executableDirectory(argv[0]); @@ -406,6 +409,12 @@ int main(int argc, char** argv) { } ++frames; } + const auto completedTicks = world.snapshot().tick; + // Run normal shutdown while diagnostics are still observable. Runtime's + // destructor is a fallback and cannot print messages after this scope ends. + world.clear(); + while (logCursor < world.diagnostics().size()) + std::cerr << world.diagnostics()[logCursor++] << '\n'; if (!capturePath.empty()) { if (frames == 0) throw std::runtime_error("No frame was rendered for capture"); @@ -438,8 +447,8 @@ int main(int argc, char** argv) { "renderer_readback_cpu measures map/copy/unmap wall time within that call. " "GPU timestamps cover submitted rendering, not CPU work. A missing/zero GPU " "timestamp is null. Frame durations exclude profile bookkeeping and final " - "capture/profile file writes. Startup begins at main(), excluding OS " - "loader/launcher."}, + "capture/profile file writes. Startup begins at Player application entry " + "after platform argument normalization, excluding OS loader/launcher."}, {"startup_ms", {{"scene_read", milliseconds(sceneReadStarted, sceneReadFinished)}, {"world_initialization", milliseconds(worldStarted, rendererStarted)}, @@ -448,7 +457,7 @@ int main(int argc, char** argv) { faset::atomic_write_json(profilePath, report); } std::cout << nlohmann::json{{"frames", frames}, - {"ticks", world.snapshot().tick}, + {"ticks", completedTicks}, {"dimension", document.value("dimension", 3)}, {"device", stats.device}, {"validation_errors", stats.validation_errors}} @@ -460,3 +469,13 @@ int main(int argc, char** argv) { return 1; } } + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return faset::run_utf8_main(argc, argv, player_main); +} +#else +int main(int argc, char** argv) { + return player_main(argc, argv); +} +#endif diff --git a/apps/project_launcher.cpp b/apps/project_launcher.cpp index e73ff9b..da9c80d 100644 --- a/apps/project_launcher.cpp +++ b/apps/project_launcher.cpp @@ -14,7 +14,7 @@ using ui::Kind; using ui::Widget; fs::path user_home() { #ifdef _WIN32 - if (const auto* value = std::getenv("USERPROFILE")) + if (const auto* value = _wgetenv(L"USERPROFILE"); value && *value) return fs::path(value); #else if (const auto* value = std::getenv("HOME")) @@ -24,7 +24,7 @@ fs::path user_home() { } fs::path recent_path() { #ifdef _WIN32 - if (const auto* value = std::getenv("APPDATA")) + if (const auto* value = _wgetenv(L"APPDATA"); value && *value) return fs::path(value) / "Faset/recent-projects.json"; #else if (const auto* value = std::getenv("XDG_CONFIG_HOME"); value && *value) @@ -44,20 +44,16 @@ fs::path normalize(std::string text) { throw std::runtime_error("Enter a project directory."); if (text.find_first_of("\r\n\t") != std::string::npos || text.find('\0') != std::string::npos) throw std::runtime_error("A directory must fit on one line."); - fs::path path = fs::u8path(text); + fs::path path = path_from_utf8(text); if (text == "~") path = user_home(); else if (text.starts_with("~/") || text.starts_with("~\\")) - path = user_home() / fs::u8path(text.substr(2)); + path = user_home() / path_from_utf8(text.substr(2)); path = fs::weakly_canonical(fs::absolute(path)); if (path.filename() == "project.faset.json") path = path.parent_path(); return path; } -std::string utf8(const fs::path& path) { - const auto value = path.u8string(); - return std::string(reinterpret_cast(value.data()), value.size()); -} ProjectSelection open_project(const fs::path& path) { if (!fs::is_directory(path)) throw std::runtime_error("Project directory does not exist."); @@ -87,12 +83,12 @@ Json recent_records(const fs::path& file) { } } void remember(const fs::path& project, const fs::path& store) { - const auto selected = open_project(normalize(utf8(project))); + const auto selected = open_project(normalize(path_to_utf8(project))); auto records = recent_records(store); std::erase_if(records.get_ref(), [&](const Json& record) { - return !record.is_string() || record.get() == utf8(selected.path); + return !record.is_string() || record.get() == path_to_utf8(selected.path); }); - records.insert(records.begin(), utf8(selected.path)); + records.insert(records.begin(), path_to_utf8(selected.path)); while (records.size() > 12) records.erase(records.end() - 1); atomic_write_json(store, records); @@ -115,6 +111,13 @@ struct ProjectLauncher::Impl { render::Renderer& renderer; ui::Context ui; render::Snapshot frame_data; + float ui_scale = 1; + float logical_width() const { + return float(renderer.width()) / ui_scale; + } + float logical_height() const { + return float(renderer.height()) / ui_scale; + } fs::path recents_file, browse_path; std::optional pending_browse; std::optional selected; @@ -136,7 +139,7 @@ struct ProjectLauncher::Impl { [this](bool enabled) { renderer.set_text_input(enabled); }, [this](ui::Rect r) { renderer.set_text_input_area(r.x, r.y, r.width, r.height); }); const auto start = initial.empty() ? user_home() / "FasetProjects/MyGame" : initial; - ui.update_text("launcher-path", utf8(start)); + ui.update_text("launcher-path", path_to_utf8(start)); create = initial.empty(); ui.find("launcher-open")->on_click = [this](Widget&) { set_mode(false); }; ui.find("launcher-create")->on_click = [this](Widget&) { set_mode(true); }; @@ -151,7 +154,7 @@ struct ProjectLauncher::Impl { build_browser(); load_recents(); refresh(); - ui.layout(float(renderer.width()), float(renderer.height())); + ui.layout(float(renderer.width()), float(renderer.height()), ui_scale); ui.focus(create ? "launcher-name" : "launcher-path"); } void set_mode(bool value) { @@ -175,9 +178,9 @@ struct ProjectLauncher::Impl { const auto id = "launcher-recent-" + std::to_string(i++); auto& item = list.add(Kind::TreeRow, id, selection.name); item.layout.height = 40; - item.tooltip = utf8(selection.path); + item.tooltip = path_to_utf8(selection.path); item.on_click = [this, path = selection.path](Widget&) { - ui.update_text("launcher-path", utf8(path), true); + ui.update_text("launcher-path", path_to_utf8(path), true); set_mode(false); }; } catch (...) { @@ -250,7 +253,7 @@ struct ProjectLauncher::Impl { actions.layout.height = 36; button(actions, "browser-choose", "Choose directory", [this] { if (browser_valid) { - ui.update_text("launcher-path", utf8(browse_path), true); + ui.update_text("launcher-path", path_to_utf8(browse_path), true); close_browser(); } }).layout.width = 180; @@ -277,11 +280,11 @@ struct ProjectLauncher::Impl { auto& list = *ui.find("browser-list"); list.children.clear(); list.scroll_y = 0; - ui.update_text("browser-path", utf8(browse_path), true); + ui.update_text("browser-path", path_to_utf8(browse_path), true); std::size_t index = 0; for (const auto& child : children) { auto& row = list.add(Kind::TreeRow, "browser-entry-" + std::to_string(index++), - "[Folder] " + utf8(child.filename())); + "[Folder] " + path_to_utf8(child.filename())); row.layout.height = 34; row.on_click = [this, child](Widget&) { ui.clear_focus(); @@ -327,6 +330,7 @@ struct ProjectLauncher::Impl { } } void refresh() { + ui_scale = std::clamp(renderer.display_scale(), .5f, 4.f); ui.find("launcher-open")->selected = !create; ui.find("launcher-create")->selected = create; ui.find("launcher-2d")->selected = dimension == 2; @@ -344,17 +348,17 @@ struct ProjectLauncher::Impl { ui.find("launcher-body")->enabled = !browsing; auto* dialog = ui.find("launcher-browser"); dialog->visible = browsing; - dialog->layout.width = std::max(320.f, std::min(760.f, float(renderer.width()) - 40)); - dialog->layout.height = std::max(300.f, std::min(540.f, float(renderer.height()) - 40)); - dialog->layout.x = (float(renderer.width()) - dialog->layout.width) * .5f; - dialog->layout.y = (float(renderer.height()) - dialog->layout.height) * .5f; + dialog->layout.width = std::max(320.f, std::min(760.f, logical_width() - 40)); + dialog->layout.height = std::max(300.f, std::min(540.f, logical_height() - 40)); + dialog->layout.x = (logical_width() - dialog->layout.width) * .5f; + dialog->layout.y = (logical_height() - dialog->layout.height) * .5f; ui.find("launcher-sidebar")->layout.width = - std::clamp(float(renderer.width()) * .24f, 180.f, 235.f); - ui.find("launcher-main")->layout.padding = renderer.width() < 850 ? 16 : 32; + std::clamp(logical_width() * .24f, 180.f, 235.f); + ui.find("launcher-main")->layout.padding = logical_width() < 850 ? 16 : 32; } void frame(const std::vector& events) { refresh(); - ui.layout(float(renderer.width()), float(renderer.height())); + ui.layout(float(renderer.width()), float(renderer.height()), ui_scale); for (const auto& event : events) { if (event.type == render::Event::Type::Quit) cancelled = true; @@ -371,7 +375,7 @@ struct ProjectLauncher::Impl { ui.clear_focus(); navigate_pending(); if (!ui.editing() && browser_valid) { - ui.update_text("launcher-path", utf8(browse_path), true); + ui.update_text("launcher-path", path_to_utf8(browse_path), true); close_browser(); } } else @@ -384,10 +388,10 @@ struct ProjectLauncher::Impl { ui.handle(event); navigate_pending(); refresh(); - ui.layout(float(renderer.width()), float(renderer.height())); + ui.layout(float(renderer.width()), float(renderer.height()), ui_scale); } refresh(); - ui.layout(float(renderer.width()), float(renderer.height())); + ui.layout(float(renderer.width()), float(renderer.height()), ui_scale); frame_data = {}; frame_data.clear_color = ui.theme().background; ui.draw(frame_data); diff --git a/apps/schema_exporter_main.cpp b/apps/schema_exporter_main.cpp index 396816c..f543dc4 100644 --- a/apps/schema_exporter_main.cpp +++ b/apps/schema_exporter_main.cpp @@ -3,7 +3,7 @@ #include #include -int main(int argc, char** argv) { +int schema_main(int argc, char** argv) { try { std::filesystem::path output; for (int i = 1; i < argc; ++i) { @@ -14,7 +14,7 @@ int main(int argc, char** argv) { return 0; } if (argument == "--output" && i + 1 < argc && output.empty()) - output = argv[++i]; + output = faset::path_from_utf8(argv[++i]); else throw std::invalid_argument("Unknown, repeated or incomplete argument: " + argument); @@ -33,3 +33,13 @@ int main(int argc, char** argv) { return 1; } } + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return faset::run_utf8_main(argc, argv, schema_main); +} +#else +int main(int argc, char** argv) { + return schema_main(argc, argv); +} +#endif diff --git a/cmake/EditorUI.cmake b/cmake/EditorUI.cmake index f15780d..63556c3 100644 --- a/cmake/EditorUI.cmake +++ b/cmake/EditorUI.cmake @@ -4,6 +4,15 @@ if(TARGET faset_ui AND TARGET faset_editor_session AND TARGET faset_scene_view) add_library(faset_project_launcher STATIC ${PROJECT_SOURCE_DIR}/apps/project_launcher.cpp) target_link_libraries(faset_project_launcher PUBLIC faset_ui faset_render) if(BUILD_TESTING) + add_executable(faset_editor_import_conflicts_ui_tests ${PROJECT_SOURCE_DIR}/tests/editor_ui_import_conflicts.cpp) + target_link_libraries(faset_editor_import_conflicts_ui_tests PRIVATE faset_editor_ui) + target_compile_definitions(faset_editor_import_conflicts_ui_tests PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}") + add_test(NAME editor_ui_import_conflicts COMMAND faset_editor_import_conflicts_ui_tests) + set_tests_properties(editor_ui_import_conflicts PROPERTIES LABELS "gpu") + # Invoked by verify_blender_roundtrip.py after real Blender generates its fixture. + add_executable(faset_blender_editor_probe ${PROJECT_SOURCE_DIR}/tests/editor_blender_reimport_probe.cpp) + target_link_libraries(faset_blender_editor_probe PRIVATE faset_editor_ui) + target_compile_definitions(faset_blender_editor_probe PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}") add_executable(faset_editor_project_settings_ui_tests ${PROJECT_SOURCE_DIR}/tests/editor_ui_project_settings.cpp) target_link_libraries(faset_editor_project_settings_ui_tests PRIVATE faset_editor_ui) target_compile_definitions(faset_editor_project_settings_ui_tests PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}") diff --git a/cmake/Player.cmake b/cmake/Player.cmake index 3d7978d..760a03c 100644 --- a/cmake/Player.cmake +++ b/cmake/Player.cmake @@ -20,5 +20,19 @@ if(TARGET faset_runtime AND TARGET faset_render AND TARGET faset_assets) add_executable(faset_player_tests ${PROJECT_SOURCE_DIR}/tests/runtime_player_tests.cpp) target_link_libraries(faset_player_tests PRIVATE faset_scene_view faset_runtime faset_assets) add_test(NAME player_scene_contracts COMMAND faset_player_tests) + + # Exercise the production Player entry with an isolated throwing module. + # No test-only switches or behaviors are added to the shipping Player. + add_executable(faset_player_diagnostics + ${PROJECT_SOURCE_DIR}/apps/player_main.cpp + ${PROJECT_SOURCE_DIR}/tests/player_diagnostics/Gameplay.cpp) + target_include_directories(faset_player_diagnostics PRIVATE + ${PROJECT_SOURCE_DIR}/tests/player_diagnostics) + target_link_libraries(faset_player_diagnostics PRIVATE faset_scene_view faset_runtime) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + add_test(NAME player_shutdown_diagnostics COMMAND ${Python3_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/tests/player_diagnostics_test.py + $) + set_tests_properties(player_shutdown_diagnostics PROPERTIES LABELS "gpu" TIMEOUT 60) endif() endif() diff --git a/cmake/Renderer.cmake b/cmake/Renderer.cmake index e1ac9f9..2309205 100644 --- a/cmake/Renderer.cmake +++ b/cmake/Renderer.cmake @@ -35,6 +35,10 @@ if(BUILD_TESTING) add_test(NAME render_graph COMMAND faset_render_tests --unit) add_test(NAME render_offscreen COMMAND faset_render_tests --gpu "${CMAKE_BINARY_DIR}/render-test.ppm") set_tests_properties(render_offscreen PROPERTIES LABELS "gpu") + add_executable(faset_render_sprite_tests "${PROJECT_SOURCE_DIR}/tests/render_sprite_tests.cpp") + target_link_libraries(faset_render_sprite_tests PRIVATE faset_render) + add_test(NAME render_sprite_alpha COMMAND faset_render_sprite_tests) + set_tests_properties(render_sprite_alpha PROPERTIES LABELS "gpu") add_executable(faset_render_reload_tests "${PROJECT_SOURCE_DIR}/tests/render_reload_tests.cpp") target_link_libraries(faset_render_reload_tests PRIVATE faset_render faset_core) target_compile_definitions(faset_render_reload_tests PRIVATE @@ -45,5 +49,9 @@ if(BUILD_TESTING) FASET_PYTHON_EXECUTABLE="${Python3_EXECUTABLE}") add_test(NAME render_shader_reload COMMAND faset_render_reload_tests) set_tests_properties(render_shader_reload PROPERTIES LABELS "gpu") + add_executable(faset_render_window_tests "${PROJECT_SOURCE_DIR}/tests/render_window_tests.cpp") + target_link_libraries(faset_render_window_tests PRIVATE faset_render SDL3::SDL3) + add_test(NAME render_window_lifecycle COMMAND faset_render_window_tests "${CMAKE_BINARY_DIR}/window-test") + set_tests_properties(render_window_lifecycle PROPERTIES LABELS "gpu;window" TIMEOUT 40 SKIP_RETURN_CODE 77) endif() install(FILES ${FASET_SHADER_OUTPUTS} DESTINATION shaders) diff --git a/cmake/WindowsPlatform.cmake b/cmake/WindowsPlatform.cmake new file mode 100644 index 0000000..f0ee0b0 --- /dev/null +++ b/cmake/WindowsPlatform.cmake @@ -0,0 +1,12 @@ +# Include after the Faset targets have been declared. A source manifest is merged +# with CMake's generated application manifest by its MSVC/Ninja linker wrapper. +# External build tools have their own manifests and still need a compatible host. +if(WIN32) + get_property(_faset_targets DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY BUILDSYSTEM_TARGETS) + foreach(_faset_target IN LISTS _faset_targets) + get_target_property(_faset_type "${_faset_target}" TYPE) + if(_faset_type STREQUAL "EXECUTABLE" AND _faset_target MATCHES "^faset_") + target_sources("${_faset_target}" PRIVATE "${PROJECT_SOURCE_DIR}/cmake/windows.manifest") + endif() + endforeach() +endif() diff --git a/cmake/windows.manifest b/cmake/windows.manifest new file mode 100644 index 0000000..1536e54 --- /dev/null +++ b/cmake/windows.manifest @@ -0,0 +1,8 @@ + + + + + true + + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 389840c..7709bb4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -57,6 +57,8 @@ Scene compiler переводит авторские данные в runtime и Retained UI получает состояние и уведомления от AuthoringService; состояние виджетов не заменяет состояние документа. Стили/layout отделены от команд; ключевые действия доступны из интерфейса и через editor API. Импорт, компиляция и cook не блокируют event loop. Необходимые текстовый ввод, focus, clipboard, DPI и платформенные поведения проверяются на обеих ОС как часть UI, а не считаются автоматически решёнными SDL3. +**UI-референс не является источником истины.** Сгенерированные изображения задают визуальное направление: тёмные поверхности, плотность, типографику и иерархию. Состав функций, состояние документов и поведение определяют эти контракты, PLAN и проверенные пользовательские сценарии. Референс можно менять под рабочий функционал. + ## 6. Runtime ECS, поведение и игровой цикл — принято EnTT используется для компонентов и запросов за типизированным API Faset. Простое поведение и массовая система обращаются к одному состоянию. Собственная ECS не разрабатывается на первом этапе; выбор EnTT не обещает большей скорости или дешёвой замены backend. Системы объявляют фазу, чтение/запись компонентов и внешних ресурсов, зависимости и диагностическое имя. @@ -99,7 +101,7 @@ Development и release Player содержат runtime, выбранные иг Первый путь доставки — самостоятельный каталог/архив игры и отдельно symbols. Windows и Linux собираются и проверяются собственными native workers. Clang/clang-cl не устраняют необходимость платформенных SDK/runtime; кросс-экспорт из Linux в Windows не считается готовой возможностью. Полностью побайтово воспроизводимый executable и стабильный ABI любых компиляторов не обещаются наличием build manifest. -Критерии [плана](../PLAN.md) включают цикл сцена → Inspector → MCP authoring edit → Undo → save → build → отдельный Player, повторный импорт GLB с сохранением overrides, lifecycle/handle tests и запуск на обеих ОС без Editor/Blender/MCP/SDK. Эти проверки предстоит реализовать и выполнить. +Критерии [плана](../PLAN.md) включают цикл сцена → Inspector → MCP authoring edit → Undo → save → build → отдельный Player, повторный импорт GLB с сохранением overrides, lifecycle/handle tests и запуск на обеих ОС без Editor/Blender/MCP/SDK. Выполненные проверки и незакрытые условия перечислены в [журнале реализации](IMPLEMENTATION.md); наличие теста само по себе не означает прохождения на обеих ОС. ## История решений diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index f022303..5dd8dfd 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -141,3 +141,68 @@ building its pinned software driver. A review also identified Windows Unicode pa boundaries that must be corrected before cross-platform acceptance. Clean offline build verification, final performance baselines and the final acceptance record remain open; no MVP tag has been created. + +### Checks after checkpoint 3 + +- Public commit: `d834cfad67cd81d8c4998b90c16791361ca8c0f8`. +- Linux and Windows headless CI plus strict manual passed in GitHub run + [35295534027](https://github.com/emil28092005/Faset_Engine/actions/runs/35295534027). +- A clean committed source snapshot built all native targets with external networking + disabled; all 19 CPU tests passed. The recorded build/test phase took 174.03 s. + [Offline evidence](validation/offline-linux-2026-09-18.json) distinguishes prepared + system tools from source/dependency inputs. GPU/window checks remain separate. +- The first full Windows graphics job compiled the whole Editor successfully, then + failed creating a Vulkan instance with its CI software driver. Its CPU SceneView + test also reported a texture-loading error. These are acceptance failures under + investigation, not verified Windows graphics/export support. + +## Checkpoint 4 — portability and acceptance hardening + +Implemented after concrete regression findings: + +- UTF-8 project, resource, plugin, CLI and subprocess boundaries. Windows native paths + stay wide internally; Editor/Player/SchemaExporter normalize `wmain` arguments. + File reads, hashes and atomic writes use extended native paths. All Faset Windows + executables declare long-path support; CI enables the documented developer policy. +- Normal Stop requests Player shutdown before bounded termination fallback. Real + `onDestroy` execution was observed, and a dedicated Player fixture verifies that + callback diagnostics also reach logs during shutdown. Custom component versions + are checked against the linked gameplay schema, including data-only components. +- POSIX subprocess cleanup keeps the exited leader's PID reserved while terminating + its owned process group, then reaps it. A regression verifies that a descendant + ignoring SIGTERM cannot survive normal leader exit or cancellation, without losing + the leader's stdout tail or signaling a reused process-group ID. +- Transparent sprites use stable layer/depth ordering without depth writes, avoiding + invisible sprites hiding objects behind them. GPU regressions cover alpha layering, + shader/capture paths longer than 300 characters, and window capture after resize. +- Editor/launcher DPI scaling, logical-unit layout persistence and safe cancellation + of unfinished drags on scale changes. Retained UI tests cover 1×/2× text rasterization, + clipping, hit testing and text-input rectangles. +- Manual import-removal review with exact IDs/names and explicit acceptance. Both the + reviewed candidate and active generation are checked before publishing, so stale + confirmation cannot apply a different import. The GUI regression exercises rejection, + fresh review and successful acceptance through actual widget events. +- Extended template workflow now saves through UI controls and opens a new Session, + comparing IDs, nested origins, local additions and resolved values. +- Blender verification now updates two instances in one live Editor/SceneView, preserves + placement, tint, physics and opaque gameplay fields, and retains both visible meshes + after a removal conflict. [Blender evidence](validation/blender-live-linux-2026-09-18.json). + +Observed Linux integration: **32 passed, 1 skipped, 0 failed** in the 33-test suite. +The skip is native Wayland programmatic restore, which the compositor declined; +the same lifecycle scenario passed under XWayland. ASan/UBSan passed **17/17** tests. +Strict MkDocs passed. [Recovery evidence](validation/editor-recovery-linux-2026-09-18.json) +records actual Editor termination and failed save/import/build/Player scenarios. +[Release baseline](validation/linux-release-2026-09-18/README.md) records the two +relocated Unicode-path packages and 240-frame profiles with their scope and hashes. + +Windows execution of these fixes remains a separate gate. The previous fast CPU run +isolated a 264-character cooked path failure; the new native-path fixes must pass on +Windows before that issue is considered closed there. Native OS IME composition and +moving between physical monitors still have less coverage than deterministic widget +and platform-boundary tests. No MVP tag has been created. + +The final build-service review also reproduced a publication defect: an invalid +custom-field default can pass the build stage's shallow schema check before the +Editor rejects it. Full schema validation before publishing `last_build.json` is the +next bounded correction; checkpoint 4 does not claim this gate is already complete. diff --git a/docs/TOOLCHAINS.md b/docs/TOOLCHAINS.md index 87490c4..f57dbd8 100644 --- a/docs/TOOLCHAINS.md +++ b/docs/TOOLCHAINS.md @@ -28,6 +28,14 @@ MSVC-family builds use the dynamic CRT: `/MDd` in Debug, `/MD` otherwise. Physic Player, Editor and native plugins use compatible settings. Development and export have separate CMake directories, avoiding accidental Debug/Release mixing. +Windows developer machines use the **Enable Win32 long paths** policy +(`LongPathsEnabled=1`). Faset executables carry `longPathAware` manifests. Engine +stream/hash/atomic-file boundaries also use wide extended paths, while serialized +project paths remain ordinary UTF-8. This does not change the independent path +support of CMake, Ninja, the compiler or Slang. CI records/enables the same host +profile; the build guide explains setup. A temporary path longer than 260 characters +exposed the original SceneView failure and now has dedicated regression coverage. + ## Graphics profiles The baseline requires Vulkan 1.3 with dynamic rendering and synchronization2, a @@ -69,3 +77,9 @@ and graphics driver; they do not need this development toolchain. An offline build is recorded only after a clean build directory is configured and built with network access disabled. A warm incremental build or archive checksum verification alone is not that acceptance check. + +Commit `d834cfad67cd81d8c4998b90c16791361ca8c0f8` passed this check on Linux: full +native build and 19 CPU tests in a new user/network namespace with no external +connectivity, using a `git archive` checkout and only prefetched inputs. See +[`validation/offline-linux-2026-09-18.json`](validation/offline-linux-2026-09-18.json). +Reproduce using `python3 tools/verify_offline_build.py --output .cache/offline-check`. diff --git a/docs/manual/editor/assets.md b/docs/manual/editor/assets.md index 91d6b55..ab43927 100644 --- a/docs/manual/editor/assets.md +++ b/docs/manual/editor/assets.md @@ -74,9 +74,17 @@ mesh datablocks remain shared. Repair deliberately duplicated material IDs in Blender's Custom Properties. Save the `.blend` again. Removing an exported output produces an import conflict. Faset keeps the old -generation active. Inspect the diagnostics and update affected references before -explicitly importing with `allow_removed_outputs: true`. This accepts removal; -it does not automatically remap references. A normal GLB without persistent custom +generation active. In **Jobs**, choose **Review removals** to open **Conflicts**. +Review the removed stable IDs and their last-good names; **Copy removed IDs** helps +locate references. Update affected references, then use **Accept reviewed removal**. +If source data or the active generation has changed since review, the acceptance +fails safely; use **Reimport / review again** and inspect the new result. + +The same explicit API action uses `allow_removed_outputs: true`, with +`expected_generation` and `expected_active_generation` taken from the reviewed +job's `result.generation` and `result.previous_generation`. Use the same source and +settings. This accepts removal; it does not automatically remap references or delete +scene components. A normal GLB without persistent custom IDs uses structural matching, which cannot guarantee identity after rename or restructuring. @@ -118,3 +126,7 @@ For an end-to-end example, open `examples/projects/collect-3d`. Its exit arch in a `.blend`, reproducible Blender script and a published bundle. The repository's `tools/verify_blender_roundtrip.py` runs the actual Blender helper and Editor imports to check rename, geometry changes, removal conflicts and failure preservation. +Pass `--ui-probe build/linux-debug/faset_blender_editor_probe` to also test two +instances in one live editor session: both receive new geometry while preserving +placement, tint, opaque gameplay fields and the authoring revision. This additional +probe requires Vulkan and produces before/after captures. diff --git a/docs/manual/editor/export.md b/docs/manual/editor/export.md index b4904f5..4ac96bb 100644 --- a/docs/manual/editor/export.md +++ b/docs/manual/editor/export.md @@ -80,9 +80,12 @@ and package execution tests; that is separate from physical GPU-driver testing. ## Troubleshooting -**Unknown component or schema version:** enable/register the missing runtime module -and rebuild. The Editor preserves its data as opaque authoring fields, but export -requires a matching runtime implementation. +**Unknown component:** enable/register the missing runtime module and rebuild. A +data-only custom component still needs a schema; it does not need callbacks. + +**Schema version mismatch:** match the linked gameplay schema to the document or +apply an explicit data migration, then rebuild. The Editor preserves future or +missing component data as opaque fields; export requires a matching schema. **Missing asset:** import the original source or Blender manifest again. Preserve its sidecar so the AssetId remains stable. A cache copied from another project is not a diff --git a/docs/manual/editor/profiling.md b/docs/manual/editor/profiling.md index 915fa3e..33c0fe7 100644 --- a/docs/manual/editor/profiling.md +++ b/docs/manual/editor/profiling.md @@ -23,8 +23,9 @@ resolution, validation activation, fixed ticks and timestep. A bounded run advan one synthetic fixed timestep per frame; it does not reproduce a real-time input session. Keep that distinction when comparing runs. -Startup starts at `main()` and ends at the first completed frame. OS process loading -before `main()` is excluded. Frame wall times exclude writing the final profile and +Startup starts at the Player application entry after platform argument normalization +and ends at the first completed frame. OS process loading and Windows `wmain` UTF-8 +argument conversion are excluded. Frame wall times exclude writing the final profile and capture files. Simulation and scene-snapshot times are separate from the renderer call. Renderer CPU wall duration includes GPU waits and readback; it is **not CPU utilization**. GPU timestamps measure the submitted graphics work and can be null diff --git a/docs/manual/editor/workspace.md b/docs/manual/editor/workspace.md index 70ae41d..bde1f97 100644 --- a/docs/manual/editor/workspace.md +++ b/docs/manual/editor/workspace.md @@ -1,5 +1,10 @@ # Editor workspace +The Editor follows the window's display scale. Text is rasterized at that scale; +panel dimensions are saved in logical units so the layout remains usable when +moving between displays. A scale change preserves focused text and cancels an +unfinished numeric, divider or gizmo drag without committing an edit. + The Editor edits saved scene data and previews it in a Vulkan viewport. **Play** opens a separate Player process. Gameplay movement and spawned objects stay in that process; stopping it leaves the authoring scene unchanged. diff --git a/docs/manual/getting-started/build.md b/docs/manual/getting-started/build.md index db86d4e..49afce8 100644 --- a/docs/manual/getting-started/build.md +++ b/docs/manual/getting-started/build.md @@ -67,6 +67,21 @@ Use an x64 Visual Studio Developer shell with the Windows SDK, MSVC runtime libr LLVM `clang-cl`, Ninja, CMake, and the Vulkan SDK available. Then use the `windows-debug` or `windows-release` presets. +Enable **Win32 long paths** on the Windows development machine before starting the +build shell. Faset's executable manifest declares long-path support, and its direct +file IO uses wide extended paths; external CMake/Ninja/compiler tools also need a +compatible host policy for deeply nested build/cache directories. An administrator +can enable the policy once in PowerShell: + +```powershell +New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' ` + -Name LongPathsEnabled -Value 1 -PropertyType DWORD -Force +``` + +Open a new build shell afterwards; Windows may require a restart for existing +processes. See Microsoft's [long-path requirements](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation). +Windows CI enables and records this developer profile explicitly. + ```powershell py tools/fetch_slang.py cmake --preset windows-debug diff --git a/docs/manual/scripting/api.md b/docs/manual/scripting/api.md index 62a65e0..4456316 100644 --- a/docs/manual/scripting/api.md +++ b/docs/manual/scripting/api.md @@ -4,7 +4,7 @@ Include `` and use namespace `faset::runtime`. This i ## Register behavior -`void Runtime::registerBehavior(std::string componentType, Behavior behavior)` registers callbacks before scene loading. An empty or duplicate type, registration during a callback, and registration after entities have loaded are rejected. +`void Runtime::registerBehavior(std::string componentType, Behavior behavior)` registers callbacks before scene loading. An empty or duplicate type and registration while entities are present or a callback is running are rejected. An empty or cleared world can register additional types. `Behavior::Callback` is `std::function`. Assign it to any of `onStart`, `fixedUpdate`, `update`, `lateUpdate`, and `onDestroy`. Unassigned members do nothing. `Behavior::onCollision` instead accepts `(Runtime&, EntityHandle, const CollisionEvent&)`. @@ -63,6 +63,13 @@ Immediate validation errors throw. Deferred failures are recorded in diagnostics `Runtime(RuntimeConfig = {})` constructs the controller. `load(const nlohmann::json&)` validates and prepares a scene, creates its entities, then calls initial callbacks. Invalid scene data leaves the preceding world intact. `clear()` destroys the current entities and invalidates their session handles. +Direct callers can include `` and call +`validate_scene_schemas(scene, gameplaySchema)` before `load`. The Player does this +automatically: each custom TypeId/version must match its linked schema. Generic +`Runtime` accepts opaque custom data with positive versions, including data-only +components; built-ins use version 1. This helper verifies identity/version, without +automatic migrations or authoring-style custom-field constraint validation. + `FrameStats advance(double elapsedSeconds, InputState = {})` advances fixed ticks, frame callbacks, interpolation, and late callbacks. `singleStep(InputState = {})` advances one fixed tick. `setPaused(bool)` clears accumulated time and resets presentation history; `paused()` reports this local state. Do not call load/clear/advance recursively from a callback. `RuntimeConfig` defaults to `fixedDelta = 1.0 / 60.0`, `maxCatchUpTicks = 4`, `physicsSubsteps = 4`, and `gravity = {0, -9.81f, 0}`. `FrameStats` reports fixed ticks performed, dropped time, interpolation fraction, and total tick count. diff --git a/docs/manual/scripting/first-behavior.md b/docs/manual/scripting/first-behavior.md index cd34471..aecd996 100644 --- a/docs/manual/scripting/first-behavior.md +++ b/docs/manual/scripting/first-behavior.md @@ -42,7 +42,7 @@ Read the callback from top to bottom: 4. `transform` gives a pose copy. Multiplying metres per second by seconds gives a displacement in metres. 5. `setTransform` publishes the changed non-physical pose. -The `[](...) { ... }` expression is a C++ lambda: a function stored in `Behavior::update`. Empty brackets mean it captures no local variables. `registerBehavior` takes ownership of the callback object. Register before calling `load`; registration after a world has loaded is rejected. +The `[](...) { ... }` expression is a C++ lambda: a function stored in `Behavior::update`. Empty brackets mean it captures no local variables. `registerBehavior` takes ownership of the callback object. Register before calling `load`; registration while entities exist or a callback is running is rejected. The `schema()` function describes editable configuration. It does not create a runtime object. `tutorial.move_x` is the stable `TypeId`; `speed` is a stable `FieldId` within that type. Keep these IDs when changing a display label. Changing a field's meaning or units needs an explicit data migration, not just a new label. @@ -56,7 +56,10 @@ The example scene is a complete, loadable document: The sprite is visible because it has `faset.sprite`. It moves because it also has `tutorial.move_x`. The configuration field is `speed`; the type string must match the registration exactly. `rotation` uses radians, and the default coordinate system is Y-up. -After your project's schema is exported and loaded by the Editor, the type can be described through the same authoring schema used by the Inspector. The direct Player command above is useful before building an Editor workflow around your component. +After **Build C++** succeeds in the Editor, choose **Add component** in the Inspector +and select the registered type. Its schema supplies editable fields, defaults and +constraints. The direct Player command above is useful for testing the same behavior +independently of an editor session. ## Make a change and verify it diff --git a/docs/manual/scripting/index.md b/docs/manual/scripting/index.md index ac0ef4f..ef86519 100644 --- a/docs/manual/scripting/index.md +++ b/docs/manual/scripting/index.md @@ -23,6 +23,13 @@ A gameplay directory contains `Gameplay.hpp` and `Gameplay.cpp`. It provides two The Player calls registration before loading a scene. The separate SchemaExporter calls `schema()` without creating a game world or running gameplay callbacks. The Editor reads the resulting declaration; it does not load the gameplay binary into the Editor process. +Every custom component in a played scene must have a matching TypeId and exact +positive version in the linked gameplay `schema()`. The Player checks this before +loading the world, including `--validate`. A data-only custom component still needs +a schema, even when it registers no callbacks. Built-in TypeIds are reserved. The +Editor preserves missing or future component data for recovery, but the Player +rejects it until the corresponding module/schema is available or the data is migrated. + A scene object receives a behavior by containing a component whose `type` matches the string passed to `registerBehavior`. Registering a behavior does not attach it to every object. A component can contain data without having any callbacks. ## Data, poses, and state diff --git a/docs/manual/scripting/lifecycle.md b/docs/manual/scripting/lifecycle.md index 4e7fbff..d9a5a16 100644 --- a/docs/manual/scripting/lifecycle.md +++ b/docs/manual/scripting/lifecycle.md @@ -10,6 +10,12 @@ Each ordinary callback receives `(Runtime& game, EntityHandle self, double delta `onDestroy` runs while that object's handle and allowed component data are still valid. Clean up subscriptions or external C++ state there. After removal, `valid(oldHandle)` returns false. Removing a behavior component also runs that component's `onDestroy`. Clearing or replacing a scene runs destruction callbacks; a new scene uses a new session identity. +Normal Player shutdown and the Editor's **Stop** request also run destruction callbacks. +Stop gives the Player a bounded grace period to finish. An unresponsive native callback +can force the Editor to terminate the process; no application can guarantee cleanup +callbacks after forced termination or a crash. Such fallback is reported in Console. +Exceptions caught from callbacks, including shutdown callbacks, appear in Player logs. + Registering a callback does not make captured pointers safe. A lambda that stores a reference to a stack variable in `registerGameplay` will outlive that variable. The [timed-despawn example](examples.md#spawn-and-destroy-on-safe-boundaries) uses shared ownership for captured state and removes each object's entry on destruction. ## One fixed tick @@ -58,4 +64,4 @@ The Player reads this optional object from the scene document. The settings are } ``` -This is an excerpt, not a complete scene. The tutorial scenes contain complete examples. `fixed_delta` is seconds, gravity is metres per second squared, and substeps are solver subdivisions inside one fixed tick. These do not create additional gameplay callbacks. Invalid configuration is rejected before the Player starts. Editing these JSON settings is implemented; an Editor settings panel should only be relied on where the current UI exposes it. +This is an excerpt, not a complete scene. The tutorial scenes contain complete examples. `fixed_delta` is seconds, gravity is metres per second squared, and substeps are solver subdivisions inside one fixed tick. These do not create additional gameplay callbacks. Invalid configuration is rejected before the Player starts. The Editor's **Simulation** dialog edits these scene settings through normal Undo transactions; **Project settings** edits the project's name, dimension and start scene separately. diff --git a/docs/validation/README.md b/docs/validation/README.md new file mode 100644 index 0000000..32d5053 --- /dev/null +++ b/docs/validation/README.md @@ -0,0 +1,14 @@ +# Validation evidence + +These files preserve bounded checks and their inputs. Each record states its source revision or working-tree limitation; a passing record does not certify later commits or every supported platform. + +- [Linux offline build](offline-linux-2026-09-18.json): a fresh build from committed `d834cfad67cd81d8c4998b90c16791361ca8c0f8`, prefetched dependency inputs, and a network namespace without external connectivity. CPU tests ran; graphics execution is separate. +- [Linux Release export and rendering](linux-release-2026-09-18/README.md): two relocated playable games, Unicode paths, imported Blender geometry, raw frame profiles and exact package hashes. This measurement used an evolving working tree around `5ac4db4`, not a clean checkout of the current commit. +- [Live Blender reimport](blender-live-linux-2026-09-18.json): actual Blender 4.5.3 exports and two live Editor instances, preserving authoring/gameplay/physics while refreshing geometry. +- [Editor recovery](editor-recovery-linux-2026-09-18.json): killed Editor, explicit journal recovery, failed save/import/build and failed Player startup with last-good preservation. +- [Native window boundaries](native-window-linux-2026-09-18.json): XWayland lifecycle, isolated Unicode clipboard, SDL text-input rectangles and the explicit native Wayland restore limitation. +- [Linux native window and input boundaries](native-window-linux-2026-09-18.json): XWayland resize/minimize/restore and SDL text-input-area checks; isolated Xvfb Unicode clipboard roundtrip with restoration. Native Wayland restore remains unconfirmed; real OS IME composition was not tested. + +The Linux records do not establish Windows correctness, physical Windows GPU performance, native Wayland restore support, or editor responsiveness under large workloads. Windows CI artifacts record their own commit and software Vulkan results. A skipped compositor operation is not a completed acceptance check. + +Performance budgets below are proposals for P1 regression tracking, not completed release requirements. Repeat measurements on a controlled reference host and add representative content before treating them as engine-wide targets. diff --git a/docs/validation/blender-live-linux-2026-09-18.json b/docs/validation/blender-live-linux-2026-09-18.json new file mode 100644 index 0000000..edb72c7 --- /dev/null +++ b/docs/validation/blender-live-linux-2026-09-18.json @@ -0,0 +1,23 @@ +{ + "blender": "Blender 4.5.3 LTS", + "asset_id": "54c039e4-c19f-4365-a9f8-a38416ec6e3f", + "initial_generation": "499f254ce440ecb86118f2f6803f21013e763d35cfdba1c4356be7a457eec718", + "renamed_generation": "963e996b59ba11c2f9dd9eba51b89e7ae3f3dce6f177e4adaf5682479cb204a8", + "stable_output_ids": true, + "removed_output_conflict": true, + "failed_import_keeps_generation": true, + "authoring_preserved": true, + "live_editor": { + "conflict_keeps_visible_generation": true, + "instances": 2, + "live_geometry_updated": true, + "opaque_gameplay_preserved": true, + "physics_fields_preserved": true, + "placement_and_tint_preserved": true, + "status": "passed", + "validation_enabled": true, + "validation_errors": 0 + }, + "source_context": "Shared Linux checkpoint 4 working tree around 5ac4db4; not a clean-commit result", + "reproduce": "python3 tools/verify_blender_roundtrip.py --blender PATH --editor build/linux-debug/faset_editor --ui-probe build/linux-debug/faset_blender_editor_probe --output NEW_DIRECTORY" +} diff --git a/docs/validation/editor-recovery-linux-2026-09-18.json b/docs/validation/editor-recovery-linux-2026-09-18.json new file mode 100644 index 0000000..b74de01 --- /dev/null +++ b/docs/validation/editor-recovery-linux-2026-09-18.json @@ -0,0 +1,33 @@ +{ + "format": "faset.editor-recovery-verification", + "version": 1, + "status": "passed", + "checks": [ + "acknowledged unsaved edit survives killed Editor and explicit recovery", + "failed save preserves live authoring and last saved file", + "failed Player process is reported without changing authoring data", + "Undo repairs the scene, then Play and Stop leave authoring unchanged", + "failed C++ build keeps prior schema and reports stale status", + "fixed C++ rebuild restores current schema status", + "failed import preserves authoring" + ], + "logs": [ + "build completed", + "Gameplay schema loaded", + "Play started in a separate Player process", + "Player failed: unsupported component version\n", + "Player exited with code 1", + "build completed", + "Gameplay schema loaded", + "Play started in a separate Player process", + "New round: collect the three gold cubes, then reach the green exit. E resets.\n{\"device\":\"NVIDIA GeForce RTX 2080 Ti\",\"dimension\":2,\"frames\":382,\"ticks\":103,\"validation_errors\":0}\n", + "Play stopped; authoring scene unchanged", + "build failed: Process exited with code 1; see job log; previous gameplay metadata remains available and is marked stale", + "build completed", + "Gameplay schema loaded", + "Asset import failed: Cannot read: /home/emil/Desktop/Faset_Engine/.cache/recovery-checkpoint3-v3/project/Assets/missing.glb\n" + ], + "engine_revision": "d834cfad67cd81d8c4998b90c16791361ca8c0f8", + "execution_context": "Linux development desktop; Editor and source from the clean offline checkout; disposable sample project", + "scope": "Crash recovery and last-good preservation. Graceful OnDestroy is verified separately by the later Player shutdown regression." +} diff --git a/docs/validation/linux-release-2026-09-18/README.md b/docs/validation/linux-release-2026-09-18/README.md new file mode 100644 index 0000000..fec6304 --- /dev/null +++ b/docs/validation/linux-release-2026-09-18/README.md @@ -0,0 +1,33 @@ +# Linux Release export baseline — 2026-09-18 + +Both checked-in playable projects passed real Editor export, standalone CPU validation and 120 offscreen frames after relocation. Their disposable source-project paths were hidden during launch; no `--scene` or `--assets` override was supplied. Copied project and executable paths contain `Faset Café 世界`. The 3D export includes the Blender exit-arch asset. + +This is a measurement of the shared working tree around `5ac4db438dae5ee28e5619e62cf8d6647beecd52` plus in-progress checkpoint 4 changes. It is **not an exact clean-commit result**. The two Release generations are `48ec8546-3750-463f-9c4c-f76c3b933256` (2D) and `17281e17-9f4f-44f0-8821-6e0071e9a45b` (3D). [Evidence](evidence.json) retains project input hashes and build fingerprints; [2D](collect-2d-manifest.json) and [3D](collect-3d-manifest.json) manifests retain the exact executable, cooked data and shader hashes. + +The same relocated executables then recorded 240 frames each, sequentially, without physics debug drawing or player input. Hardware: Ryzen 7 1700, 32 GiB RAM, RTX 2080 Ti 11 GiB, NVIDIA 595.84; Ubuntu 26.04, kernel 7.0.0-31. Build: Clang 21.1.8, Release `-O3 -DNDEBUG`, Vulkan 1.3, 1280×720, actual Khronos validation enabled. Both runs had zero validation errors. The desktop remained active; this was not an isolated benchmark machine. + +Raw [2D](collect-2d-profile-240.json) and [3D](collect-3d-profile-240.json) profiles include every completed frame, including the first, with nearest-rank percentiles. Times below are p50 / p95 milliseconds: + +- 2D: total measured frame 2.234 / 2.684; renderer call 1.865 / 2.221; GPU 0.565 / 0.579; readback 0.510 / 0.633; simulation 0.201 / 0.258; snapshot 0.170 / 0.215. +- 3D: total measured frame 2.299 / 2.732; renderer call 1.884 / 2.180; GPU 0.588 / 0.600; readback 0.469 / 0.608; simulation 0.213 / 0.300; snapshot 0.181 / 0.257. + +Live Vulkan allocations stayed at 15,750,288 bytes (2D) and 15,787,008 bytes (3D), about 15.02 and 15.06 MiB. Counts were 11 / 31 draws, 66 / 576 packed vertices per frame, and one fallback texture. A packed mesh can be drawn again for its shadow pass. These memory totals include aligned explicit allocations and exclude driver internals. First-frame startup from `main()` was 265.5 / 260.8 ms; OS loader time is excluded. + +`render_call` includes GPU waits and synchronous readback; it is not CPU utilization. GPU timestamps cover submitted rendering. Frame timings exclude profile bookkeeping and final file writes. Synthetic 1/60-second simulation ticks run as fast as the offscreen loop allows. The resulting numbers are not display FPS or a real-time gameplay pacing test. Small sample scenes, one GPU, 240 frames and a static camera cannot establish scalability, a memory-leak guarantee, or broad performance claims. The imported 3D material also emits the recorded warning about unsupported material features in its manifest. + +Proposed P1 budgets for these exact scenes on this reference-class Linux host, at the same resolution and validation/readback settings: + +- Measured frame p95 ≤ 4 ms; GPU p95 ≤ 1 ms; readback p95 ≤ 1 ms. +- Simulation and snapshot p95 ≤ 0.5 ms each. +- Explicit live Vulkan memory ≤ 20 MiB, with no growth after warmup over a future 3,000-frame resource-lifecycle run. +- Startup from `main()` ≤ 500 ms for these tiny packages. + +These initial thresholds leave headroom above the observed values. They are **proposed**, not enforced acceptance criteria. P1 should retain cold-start results separately, run repeated sessions, measure editor input latency and larger content, and collect independent Windows hardware baselines before adopting release gates. + +Reproduce export validation from the repository with `python tools/verify_playable_exports.py --editor build/linux-debug/faset_editor --output `. Run each retained standalone Player with `--headless --frames 240 --profile ` to repeat the longer measurement. A new run builds current sources and produces new immutable generations; use the stored hashes to distinguish it from this record. + +Captured 120-frame outputs: + +![Relocated 2D sample](collect-2d.png) + +![Relocated 3D sample with imported Blender arch](collect-3d.png) diff --git a/docs/validation/linux-release-2026-09-18/collect-2d-manifest.json b/docs/validation/linux-release-2026-09-18/collect-2d-manifest.json new file mode 100644 index 0000000..929d99a --- /dev/null +++ b/docs/validation/linux-release-2026-09-18/collect-2d-manifest.json @@ -0,0 +1,143 @@ +{ + "asset_generations": {}, + "build_fingerprint": "5b1996b0fe44eeaa01ee1fd581f9f063f8c84147128e896541d27207dbab9aaa", + "configuration": "Release", + "executable": "faset_player", + "files": [ + { + "path": "scene.fscene", + "sha256": "811242b85ede88c6d7920eeda5b7f6ae189db4d9e17d568cdfd230f27ce629b6", + "size": 6496 + }, + { + "path": "faset_player", + "sha256": "ef12daa3a1e71ef9e9d933cfa0347249de86b5e8da3536e709a3899a731e4333", + "size": 5809704 + }, + { + "path": "shaders/vertexMain.spv", + "sha256": "1ad2631c35d654f48166321ae43d4165043e1b919b14f1dc61caf97b1ada0898", + "size": 1224 + }, + { + "path": "shaders/fragmentMain.spv", + "sha256": "f5c4f289917ecab5d00d543c053aac1d10e6cca311fd172daf06d315160a3505", + "size": 7436 + }, + { + "path": "shaders/shadowMain.spv", + "sha256": "7643b4d688492b5ee923b9606f0b0e70343ca05fa67673838206119a6f86d8d8", + "size": 784 + }, + { + "path": "shaders/vertexMain.reflection.json", + "sha256": "c584f3b957ebf194a292451362a98590f85ff3cfbfd8f917b1c1265039381777", + "size": 2639 + }, + { + "path": "shaders/fragmentMain.reflection.json", + "sha256": "72ebf8d32452830650416dc4717d89a4324e97ece7028dfb97c66803c8dce71f", + "size": 2649 + }, + { + "path": "shaders/shadowMain.reflection.json", + "sha256": "e3fd0889cd2f569d410f5d7558a4091d9f57f247a1dcf1788462f67525340819", + "size": 2645 + }, + { + "path": "Notices/sdl3/LICENSE.txt", + "sha256": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127", + "size": 884 + }, + { + "path": "Notices/entt/LICENSE", + "sha256": "0785027ce472d7c61f05fba664a1d3ba6639c1593ced351ad9e4bed868765d99", + "size": 1097 + }, + { + "path": "Notices/box2d/LICENSE", + "sha256": "68a3e676d7e94093b102d5cba0d4e04af812040d6f230c3db67a6664574e43d2", + "size": 1067 + }, + { + "path": "Notices/box3d/LICENSE", + "sha256": "da5e31a26bf3cfd5ba5c96d6823e480128c81e76c107ef9d3ee5d94789184b90", + "size": 1067 + }, + { + "path": "Notices/json/LICENSE.MIT", + "sha256": "46a65cffd1ea955132d95a8dd921640714a8d6b537d2e4e482d31145ae95b603", + "size": 1076 + }, + { + "path": "Notices/stb/LICENSE", + "sha256": "bebfe904b14301657e4e5d655c811d51fd31b97c455b9cc2d8600d6bac6cff63", + "size": 2510 + }, + { + "path": "Notices/dependencies.json", + "sha256": "a54639ad2d9ad13c2e5f1ec90d6775f5dfc493656789b78c7e44debe2a378fdc", + "size": 2206 + }, + { + "path": "Notices/Faset-NOTICE.txt", + "sha256": "30c2e7e80f5153b31ab57675e07246aef9165965d64e36022b426a26449ece3f", + "size": 204 + }, + { + "path": "README.txt", + "sha256": "4dc2f0271c69fe39017ff96aa2dfa00b625bec1401d924dc396193ce71c09f3e", + "size": 225 + } + ], + "format": "faset.export", + "generation": "48ec8546-3750-463f-9c4c-f76c3b933256", + "platform": "linux", + "prerequisites": [ + "Linux x86_64", + "Vulkan 1.3 driver", + "Compatible glibc and libstdc++ runtime" + ], + "renderer_profile": { + "api": "Vulkan 1.3", + "materials": [ + "base-color factor and texture", + "metallic and roughness factors" + ], + "required_features": [ + "dynamicRendering", + "synchronization2" + ], + "shadow_map": { + "resolution": 1024, + "world_extent": 40 + }, + "texture_sampling": "linear clamp, one mip level", + "unsupported_material_features": [ + "normal maps", + "metallic-roughness maps", + "emissive and occlusion maps", + "alpha mode selection", + "unlit mode", + "per-material face culling" + ] + }, + "scene_hash": "05ab2c3e5b152ab25867976c47635961f440f41d8cd0408732eb2d8d5b7800a8", + "simulation": { + "fixed_delta": 0.016666666666666666, + "gravity": [ + 0, + -9.81, + 0 + ], + "max_catch_up_ticks": 4, + "physics_substeps": 4 + }, + "units": { + "angle": "radian", + "coordinates": "right-handed Y-up", + "distance": "metre" + }, + "validation_log": "{\"dimension\":2,\"validated\":true}\n", + "version": 1 +} diff --git a/docs/validation/linux-release-2026-09-18/collect-2d-profile-240.json b/docs/validation/linux-release-2026-09-18/collect-2d-profile-240.json new file mode 100644 index 0000000..8564796 --- /dev/null +++ b/docs/validation/linux-release-2026-09-18/collect-2d-profile-240.json @@ -0,0 +1,4637 @@ +{ + "completed_frames": 240, + "device": "NVIDIA GeForce RTX 2080 Ti", + "dimension": 2, + "fixed_delta_seconds": 0.016666666666666666, + "format": "faset.player-profile", + "height": 720, + "percentile_method": "nearest_rank_all_completed_frames_no_warmup_exclusion", + "presentation_mode": "offscreen", + "requested_frames": 240, + "resource_notes": "gpu_allocated_bytes sums live Vulkan memory allocations, including alignment, excluding driver internals. texture_count includes the white fallback texture.", + "samples": [ + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 1, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57648, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 4.700939, + "renderer_cpu_ms": 4.697233, + "renderer_readback_cpu_ms": 2.763424, + "simulation_ms": 0.181503, + "snapshot_ms": 0.172385, + "texture_count": 1, + "tick": 1, + "vertices": 66, + "wall_ms": 5.147562 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 2, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.883564, + "renderer_cpu_ms": 1.880668, + "renderer_readback_cpu_ms": 0.50899, + "simulation_ms": 0.179158, + "snapshot_ms": 0.140685, + "texture_count": 1, + "tick": 2, + "vertices": 66, + "wall_ms": 2.214568 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 3, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565824, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.804534, + "renderer_cpu_ms": 1.801749, + "renderer_readback_cpu_ms": 0.495245, + "simulation_ms": 0.165462, + "snapshot_ms": 0.147969, + "texture_count": 1, + "tick": 3, + "vertices": 66, + "wall_ms": 2.126342 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 4, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.702482, + "renderer_cpu_ms": 1.699928, + "renderer_readback_cpu_ms": 0.449247, + "simulation_ms": 0.202873, + "snapshot_ms": 0.130826, + "texture_count": 1, + "tick": 4, + "vertices": 66, + "wall_ms": 2.045379 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 5, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573952, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.601962, + "renderer_cpu_ms": 1.599228, + "renderer_readback_cpu_ms": 0.449468, + "simulation_ms": 0.177525, + "snapshot_ms": 0.132049, + "texture_count": 1, + "tick": 5, + "vertices": 66, + "wall_ms": 1.920984 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 6, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.771793, + "renderer_cpu_ms": 1.768827, + "renderer_readback_cpu_ms": 0.446583, + "simulation_ms": 0.180811, + "snapshot_ms": 0.128061, + "texture_count": 1, + "tick": 6, + "vertices": 66, + "wall_ms": 2.089953 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 7, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56432, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.71184, + "renderer_cpu_ms": 1.708413, + "renderer_readback_cpu_ms": 0.5267539999999999, + "simulation_ms": 0.160442, + "snapshot_ms": 0.153249, + "texture_count": 1, + "tick": 7, + "vertices": 66, + "wall_ms": 2.050349 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 8, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573216, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.829572, + "renderer_cpu_ms": 1.826787, + "renderer_readback_cpu_ms": 0.48232, + "simulation_ms": 0.192814, + "snapshot_ms": 0.161906, + "texture_count": 1, + "tick": 8, + "vertices": 66, + "wall_ms": 2.193208 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 9, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564864, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.788043, + "renderer_cpu_ms": 1.784907, + "renderer_readback_cpu_ms": 0.480196, + "simulation_ms": 0.197502, + "snapshot_ms": 0.152258, + "texture_count": 1, + "tick": 9, + "vertices": 66, + "wall_ms": 2.148834 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 10, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.572512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.702742, + "renderer_cpu_ms": 1.699947, + "renderer_readback_cpu_ms": 0.502929, + "simulation_ms": 0.177595, + "snapshot_ms": 0.139583, + "texture_count": 1, + "tick": 10, + "vertices": 66, + "wall_ms": 2.028557 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 11, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.835203, + "renderer_cpu_ms": 1.831606, + "renderer_readback_cpu_ms": 0.491247, + "simulation_ms": 0.191722, + "snapshot_ms": 0.14859, + "texture_count": 1, + "tick": 11, + "vertices": 66, + "wall_ms": 2.185894 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 12, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564928, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.752195, + "renderer_cpu_ms": 1.748739, + "renderer_readback_cpu_ms": 0.469135, + "simulation_ms": 0.179759, + "snapshot_ms": 0.16945, + "texture_count": 1, + "tick": 12, + "vertices": 66, + "wall_ms": 2.111403 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 13, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.698615, + "renderer_cpu_ms": 1.695128, + "renderer_readback_cpu_ms": 0.480487, + "simulation_ms": 0.190138, + "snapshot_ms": 0.144994, + "texture_count": 1, + "tick": 13, + "vertices": 66, + "wall_ms": 2.042373 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 14, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.823871, + "renderer_cpu_ms": 1.821096, + "renderer_readback_cpu_ms": 0.53556, + "simulation_ms": 0.197763, + "snapshot_ms": 0.143471, + "texture_count": 1, + "tick": 14, + "vertices": 66, + "wall_ms": 2.173019 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 15, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573568, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.689939, + "renderer_cpu_ms": 1.687794, + "renderer_readback_cpu_ms": 0.478423, + "simulation_ms": 0.184648, + "snapshot_ms": 0.15356, + "texture_count": 1, + "tick": 15, + "vertices": 66, + "wall_ms": 2.037414 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 16, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.900025, + "renderer_cpu_ms": 1.896308, + "renderer_readback_cpu_ms": 0.512747, + "simulation_ms": 0.182976, + "snapshot_ms": 0.146045, + "texture_count": 1, + "tick": 16, + "vertices": 66, + "wall_ms": 2.237341 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 17, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.137533, + "renderer_cpu_ms": 2.133595, + "renderer_readback_cpu_ms": 0.554205, + "simulation_ms": 0.186241, + "snapshot_ms": 0.170341, + "texture_count": 1, + "tick": 17, + "vertices": 66, + "wall_ms": 2.503463 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 18, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.589088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.795647, + "renderer_cpu_ms": 1.792602, + "renderer_readback_cpu_ms": 0.532605, + "simulation_ms": 0.190369, + "snapshot_ms": 0.131338, + "texture_count": 1, + "tick": 18, + "vertices": 66, + "wall_ms": 2.127083 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 19, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565376, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.868626, + "renderer_cpu_ms": 1.864798, + "renderer_readback_cpu_ms": 0.500274, + "simulation_ms": 0.19547900000000001, + "snapshot_ms": 0.159701, + "texture_count": 1, + "tick": 19, + "vertices": 66, + "wall_ms": 2.232372 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 20, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.799966, + "renderer_cpu_ms": 1.79693, + "renderer_readback_cpu_ms": 0.467402, + "simulation_ms": 0.195699, + "snapshot_ms": 0.15379, + "texture_count": 1, + "tick": 20, + "vertices": 66, + "wall_ms": 2.159144 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 21, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56416, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.714124, + "renderer_cpu_ms": 1.710487, + "renderer_readback_cpu_ms": 0.515172, + "simulation_ms": 0.19073, + "snapshot_ms": 0.181873, + "texture_count": 1, + "tick": 21, + "vertices": 66, + "wall_ms": 2.096506 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 22, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56496, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.863996, + "renderer_cpu_ms": 1.86041, + "renderer_readback_cpu_ms": 0.504622, + "simulation_ms": 0.200708, + "snapshot_ms": 0.157898, + "texture_count": 1, + "tick": 22, + "vertices": 66, + "wall_ms": 2.23171 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 23, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.792051, + "renderer_cpu_ms": 1.789146, + "renderer_readback_cpu_ms": 0.465318, + "simulation_ms": 0.188756, + "snapshot_ms": 0.167516, + "texture_count": 1, + "tick": 23, + "vertices": 66, + "wall_ms": 2.157641 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 24, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56848, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.693906, + "renderer_cpu_ms": 1.691451, + "renderer_readback_cpu_ms": 0.508179, + "simulation_ms": 0.183537, + "snapshot_ms": 0.149051, + "texture_count": 1, + "tick": 24, + "vertices": 66, + "wall_ms": 2.035661 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 25, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.785228, + "renderer_cpu_ms": 1.782082, + "renderer_readback_cpu_ms": 0.475547, + "simulation_ms": 0.186882, + "snapshot_ms": 0.147559, + "texture_count": 1, + "tick": 25, + "vertices": 66, + "wall_ms": 2.128606 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 26, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.700889, + "renderer_cpu_ms": 1.697673, + "renderer_readback_cpu_ms": 0.444368, + "simulation_ms": 0.178337, + "snapshot_ms": 0.150514, + "texture_count": 1, + "tick": 26, + "vertices": 66, + "wall_ms": 2.039778 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 27, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564064, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.823901, + "renderer_cpu_ms": 1.820305, + "renderer_readback_cpu_ms": 0.481698, + "simulation_ms": 0.173547, + "snapshot_ms": 0.152208, + "texture_count": 1, + "tick": 27, + "vertices": 66, + "wall_ms": 2.157811 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 28, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.776602, + "renderer_cpu_ms": 1.773576, + "renderer_readback_cpu_ms": 0.457813, + "simulation_ms": 0.179289, + "snapshot_ms": 0.16982, + "texture_count": 1, + "tick": 28, + "vertices": 66, + "wall_ms": 2.134497 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 29, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56432, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.659271, + "renderer_cpu_ms": 1.655854, + "renderer_readback_cpu_ms": 0.494764, + "simulation_ms": 0.20685, + "snapshot_ms": 0.147749, + "texture_count": 1, + "tick": 29, + "vertices": 66, + "wall_ms": 2.022686 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 30, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.801519, + "renderer_cpu_ms": 1.798773, + "renderer_readback_cpu_ms": 0.488702, + "simulation_ms": 0.193856, + "snapshot_ms": 0.149562, + "texture_count": 1, + "tick": 30, + "vertices": 66, + "wall_ms": 2.153533 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 31, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.5648, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.090334, + "renderer_cpu_ms": 2.086597, + "renderer_readback_cpu_ms": 0.487751, + "simulation_ms": 0.172075, + "snapshot_ms": 0.150163, + "texture_count": 1, + "tick": 31, + "vertices": 66, + "wall_ms": 2.422791 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 32, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564608, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.761013, + "renderer_cpu_ms": 1.758367, + "renderer_readback_cpu_ms": 0.487099, + "simulation_ms": 0.224313, + "snapshot_ms": 0.212611, + "texture_count": 1, + "tick": 32, + "vertices": 66, + "wall_ms": 2.208036 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 33, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565152, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.854839, + "renderer_cpu_ms": 1.852125, + "renderer_readback_cpu_ms": 0.510042, + "simulation_ms": 0.225575, + "snapshot_ms": 0.200288, + "texture_count": 1, + "tick": 33, + "vertices": 66, + "wall_ms": 2.288708 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 34, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.950039, + "renderer_cpu_ms": 1.947053, + "renderer_readback_cpu_ms": 0.465579, + "simulation_ms": 0.221688, + "snapshot_ms": 0.201611, + "texture_count": 1, + "tick": 34, + "vertices": 66, + "wall_ms": 2.381343 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 35, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.726147, + "renderer_cpu_ms": 1.723872, + "renderer_readback_cpu_ms": 0.502578, + "simulation_ms": 0.223151, + "snapshot_ms": 0.214946, + "texture_count": 1, + "tick": 35, + "vertices": 66, + "wall_ms": 2.172879 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 36, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.878625, + "renderer_cpu_ms": 1.875438, + "renderer_readback_cpu_ms": 0.528958, + "simulation_ms": 0.247537, + "snapshot_ms": 0.188516, + "texture_count": 1, + "tick": 36, + "vertices": 66, + "wall_ms": 2.322412 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 37, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.009742, + "renderer_cpu_ms": 2.006686, + "renderer_readback_cpu_ms": 0.529258, + "simulation_ms": 0.226427, + "snapshot_ms": 0.198655, + "texture_count": 1, + "tick": 37, + "vertices": 66, + "wall_ms": 2.444101 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 38, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564352, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.765892, + "renderer_cpu_ms": 1.762776, + "renderer_readback_cpu_ms": 0.454818, + "simulation_ms": 0.223391, + "snapshot_ms": 0.210147, + "texture_count": 1, + "tick": 38, + "vertices": 66, + "wall_ms": 2.209819 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 39, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.903701, + "renderer_cpu_ms": 1.901317, + "renderer_readback_cpu_ms": 0.516394, + "simulation_ms": 0.235715, + "snapshot_ms": 0.190529, + "texture_count": 1, + "tick": 39, + "vertices": 66, + "wall_ms": 2.339473 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 40, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.586944, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.877703, + "renderer_cpu_ms": 1.874908, + "renderer_readback_cpu_ms": 0.486187, + "simulation_ms": 0.215577, + "snapshot_ms": 0.194356, + "texture_count": 1, + "tick": 40, + "vertices": 66, + "wall_ms": 2.296884 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 41, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564288, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.728321, + "renderer_cpu_ms": 1.725685, + "renderer_readback_cpu_ms": 0.497108, + "simulation_ms": 0.21745, + "snapshot_ms": 0.217902, + "texture_count": 1, + "tick": 41, + "vertices": 66, + "wall_ms": 2.172349 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 42, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565152, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.930262, + "renderer_cpu_ms": 1.926184, + "renderer_readback_cpu_ms": 0.533396, + "simulation_ms": 0.230835, + "snapshot_ms": 0.192073, + "texture_count": 1, + "tick": 42, + "vertices": 66, + "wall_ms": 2.362858 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 43, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564608, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.901788, + "renderer_cpu_ms": 1.899323, + "renderer_readback_cpu_ms": 0.449178, + "simulation_ms": 0.220666, + "snapshot_ms": 0.199587, + "texture_count": 1, + "tick": 43, + "vertices": 66, + "wall_ms": 2.33227 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 44, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.786741, + "renderer_cpu_ms": 1.783996, + "renderer_readback_cpu_ms": 0.494313, + "simulation_ms": 0.226127, + "snapshot_ms": 0.183466, + "texture_count": 1, + "tick": 44, + "vertices": 66, + "wall_ms": 2.205572 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 45, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.914282, + "renderer_cpu_ms": 1.911135, + "renderer_readback_cpu_ms": 0.505885, + "simulation_ms": 0.224393, + "snapshot_ms": 0.188807, + "texture_count": 1, + "tick": 45, + "vertices": 66, + "wall_ms": 2.336408 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 46, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.58912, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.873836, + "renderer_cpu_ms": 1.87117, + "renderer_readback_cpu_ms": 0.471209, + "simulation_ms": 0.225997, + "snapshot_ms": 0.194146, + "texture_count": 1, + "tick": 46, + "vertices": 66, + "wall_ms": 2.303025 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 47, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.786982, + "renderer_cpu_ms": 1.784657, + "renderer_readback_cpu_ms": 0.52504, + "simulation_ms": 0.211559, + "snapshot_ms": 0.19617, + "texture_count": 1, + "tick": 47, + "vertices": 66, + "wall_ms": 2.203307 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 48, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.926765, + "renderer_cpu_ms": 1.92396, + "renderer_readback_cpu_ms": 0.509932, + "simulation_ms": 0.238691, + "snapshot_ms": 0.193214, + "texture_count": 1, + "tick": 48, + "vertices": 66, + "wall_ms": 2.368358 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 49, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.893071, + "renderer_cpu_ms": 1.890026, + "renderer_readback_cpu_ms": 0.468464, + "simulation_ms": 0.236216, + "snapshot_ms": 0.196851, + "texture_count": 1, + "tick": 49, + "vertices": 66, + "wall_ms": 2.336017 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 50, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.769459, + "renderer_cpu_ms": 1.766643, + "renderer_readback_cpu_ms": 0.474044, + "simulation_ms": 0.228741, + "snapshot_ms": 0.196942, + "texture_count": 1, + "tick": 50, + "vertices": 66, + "wall_ms": 2.2045 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 51, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.931864, + "renderer_cpu_ms": 1.928869, + "renderer_readback_cpu_ms": 0.515553, + "simulation_ms": 0.22229, + "snapshot_ms": 0.177725, + "texture_count": 1, + "tick": 51, + "vertices": 66, + "wall_ms": 2.339984 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 52, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.581504, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.85518, + "renderer_cpu_ms": 1.852365, + "renderer_readback_cpu_ms": 0.468093, + "simulation_ms": 0.209866, + "snapshot_ms": 0.195288, + "texture_count": 1, + "tick": 52, + "vertices": 66, + "wall_ms": 2.270284 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 53, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.797972, + "renderer_cpu_ms": 1.795277, + "renderer_readback_cpu_ms": 0.509912, + "simulation_ms": 0.223482, + "snapshot_ms": 0.19637, + "texture_count": 1, + "tick": 53, + "vertices": 66, + "wall_ms": 2.226671 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 54, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.900486, + "renderer_cpu_ms": 1.89733, + "renderer_readback_cpu_ms": 0.502258, + "simulation_ms": 0.217551, + "snapshot_ms": 0.20155, + "texture_count": 1, + "tick": 54, + "vertices": 66, + "wall_ms": 2.328112 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 55, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.575392, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.876149, + "renderer_cpu_ms": 1.873204, + "renderer_readback_cpu_ms": 0.487269, + "simulation_ms": 0.218823, + "snapshot_ms": 0.198314, + "texture_count": 1, + "tick": 55, + "vertices": 66, + "wall_ms": 2.301963 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 56, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.778616, + "renderer_cpu_ms": 1.776091, + "renderer_readback_cpu_ms": 0.502668, + "simulation_ms": 0.226397, + "snapshot_ms": 0.203013, + "texture_count": 1, + "tick": 56, + "vertices": 66, + "wall_ms": 2.217003 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 57, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.930622, + "renderer_cpu_ms": 1.927928, + "renderer_readback_cpu_ms": 0.51439, + "simulation_ms": 0.21747, + "snapshot_ms": 0.219354, + "texture_count": 1, + "tick": 57, + "vertices": 66, + "wall_ms": 2.376363 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 58, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.587872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.912909, + "renderer_cpu_ms": 1.910324, + "renderer_readback_cpu_ms": 0.509771, + "simulation_ms": 0.233972, + "snapshot_ms": 0.199356, + "texture_count": 1, + "tick": 58, + "vertices": 66, + "wall_ms": 2.355434 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 59, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564128, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.779388, + "renderer_cpu_ms": 1.776311, + "renderer_readback_cpu_ms": 0.473333, + "simulation_ms": 0.213623, + "snapshot_ms": 0.188396, + "texture_count": 1, + "tick": 59, + "vertices": 66, + "wall_ms": 2.189692 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 60, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.941583, + "renderer_cpu_ms": 1.938918, + "renderer_readback_cpu_ms": 0.527645, + "simulation_ms": 0.223382, + "snapshot_ms": 0.212851, + "texture_count": 1, + "tick": 60, + "vertices": 66, + "wall_ms": 2.387434 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 61, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573312, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.906547, + "renderer_cpu_ms": 1.904373, + "renderer_readback_cpu_ms": 0.516695, + "simulation_ms": 0.219444, + "snapshot_ms": 0.194187, + "texture_count": 1, + "tick": 61, + "vertices": 66, + "wall_ms": 2.328433 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 62, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.781732, + "renderer_cpu_ms": 1.778655, + "renderer_readback_cpu_ms": 0.501286, + "simulation_ms": 0.230866, + "snapshot_ms": 0.184448, + "texture_count": 1, + "tick": 62, + "vertices": 66, + "wall_ms": 2.206453 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 63, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57136, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.976309, + "renderer_cpu_ms": 1.9721, + "renderer_readback_cpu_ms": 0.528147, + "simulation_ms": 0.220647, + "snapshot_ms": 0.214595, + "texture_count": 1, + "tick": 63, + "vertices": 66, + "wall_ms": 2.420256 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 64, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.917268, + "renderer_cpu_ms": 1.913701, + "renderer_readback_cpu_ms": 0.473874, + "simulation_ms": 0.231868, + "snapshot_ms": 0.203794, + "texture_count": 1, + "tick": 64, + "vertices": 66, + "wall_ms": 2.362197 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 65, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56368, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.773746, + "renderer_cpu_ms": 1.771102, + "renderer_readback_cpu_ms": 0.509421, + "simulation_ms": 0.216408, + "snapshot_ms": 0.230304, + "texture_count": 1, + "tick": 65, + "vertices": 66, + "wall_ms": 2.230167 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 66, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.572768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.978322, + "renderer_cpu_ms": 1.974445, + "renderer_readback_cpu_ms": 0.553945, + "simulation_ms": 0.234522, + "snapshot_ms": 0.20141, + "texture_count": 1, + "tick": 66, + "vertices": 66, + "wall_ms": 2.424123 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 67, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.879076, + "renderer_cpu_ms": 1.87616, + "renderer_readback_cpu_ms": 0.469295, + "simulation_ms": 0.229994, + "snapshot_ms": 0.192794, + "texture_count": 1, + "tick": 67, + "vertices": 66, + "wall_ms": 2.311882 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 68, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.795888, + "renderer_cpu_ms": 1.792882, + "renderer_readback_cpu_ms": 0.521274, + "simulation_ms": 0.215627, + "snapshot_ms": 0.200919, + "texture_count": 1, + "tick": 68, + "vertices": 66, + "wall_ms": 2.221751 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 69, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.566784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.944779, + "renderer_cpu_ms": 1.941513, + "renderer_readback_cpu_ms": 0.521644, + "simulation_ms": 0.232729, + "snapshot_ms": 0.191401, + "texture_count": 1, + "tick": 69, + "vertices": 66, + "wall_ms": 2.377876 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 70, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565184, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.909413, + "renderer_cpu_ms": 1.906547, + "renderer_readback_cpu_ms": 0.494913, + "simulation_ms": 0.224684, + "snapshot_ms": 0.180801, + "texture_count": 1, + "tick": 70, + "vertices": 66, + "wall_ms": 2.323895 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 71, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.578304, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.239725, + "renderer_cpu_ms": 2.23673, + "renderer_readback_cpu_ms": 0.495976, + "simulation_ms": 0.221357, + "snapshot_ms": 0.214585, + "texture_count": 1, + "tick": 71, + "vertices": 66, + "wall_ms": 2.686619 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 72, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.947795, + "renderer_cpu_ms": 1.942906, + "renderer_readback_cpu_ms": 0.58261, + "simulation_ms": 0.229213, + "snapshot_ms": 0.193364, + "texture_count": 1, + "tick": 72, + "vertices": 66, + "wall_ms": 2.380211 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 73, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564288, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.868506, + "renderer_cpu_ms": 1.86554, + "renderer_readback_cpu_ms": 0.500023, + "simulation_ms": 0.290218, + "snapshot_ms": 0.21227, + "texture_count": 1, + "tick": 73, + "vertices": 66, + "wall_ms": 2.382295 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 74, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.587872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.786581, + "renderer_cpu_ms": 1.783755, + "renderer_readback_cpu_ms": 0.441373, + "simulation_ms": 0.182274, + "snapshot_ms": 0.133492, + "texture_count": 1, + "tick": 74, + "vertices": 66, + "wall_ms": 2.112696 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 75, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565376, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.83941, + "renderer_cpu_ms": 1.835523, + "renderer_readback_cpu_ms": 0.611504, + "simulation_ms": 0.152798, + "snapshot_ms": 0.132179, + "texture_count": 1, + "tick": 75, + "vertices": 66, + "wall_ms": 2.135068 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 76, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565184, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.851363, + "renderer_cpu_ms": 1.847716, + "renderer_readback_cpu_ms": 0.461611, + "simulation_ms": 0.389646, + "snapshot_ms": 0.208633, + "texture_count": 1, + "tick": 76, + "vertices": 66, + "wall_ms": 2.462186 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 77, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.577952, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.861252, + "renderer_cpu_ms": 1.858236, + "renderer_readback_cpu_ms": 0.462472, + "simulation_ms": 0.154671, + "snapshot_ms": 0.139203, + "texture_count": 1, + "tick": 77, + "vertices": 66, + "wall_ms": 2.164895 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 78, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.567904, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.613033, + "renderer_cpu_ms": 1.610038, + "renderer_readback_cpu_ms": 0.484654, + "simulation_ms": 0.152308, + "snapshot_ms": 0.137088, + "texture_count": 1, + "tick": 78, + "vertices": 66, + "wall_ms": 1.911607 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 79, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564928, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.72291, + "renderer_cpu_ms": 1.720005, + "renderer_readback_cpu_ms": 0.479244, + "simulation_ms": 0.150534, + "snapshot_ms": 0.138923, + "texture_count": 1, + "tick": 79, + "vertices": 66, + "wall_ms": 2.022425 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 80, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56416, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.605519, + "renderer_cpu_ms": 1.603054, + "renderer_readback_cpu_ms": 0.444058, + "simulation_ms": 0.150614, + "snapshot_ms": 0.135215, + "texture_count": 1, + "tick": 80, + "vertices": 66, + "wall_ms": 1.902208 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 81, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.739161, + "renderer_cpu_ms": 1.735895, + "renderer_readback_cpu_ms": 0.472641, + "simulation_ms": 0.145534, + "snapshot_ms": 0.135265, + "texture_count": 1, + "tick": 81, + "vertices": 66, + "wall_ms": 2.028898 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 82, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.571712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.014821, + "renderer_cpu_ms": 2.012206, + "renderer_readback_cpu_ms": 0.474275, + "simulation_ms": 0.158829, + "snapshot_ms": 0.137981, + "texture_count": 1, + "tick": 82, + "vertices": 66, + "wall_ms": 2.32155 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 83, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564256, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.640936, + "renderer_cpu_ms": 1.637299, + "renderer_readback_cpu_ms": 0.472451, + "simulation_ms": 0.154963, + "snapshot_ms": 0.127029, + "texture_count": 1, + "tick": 83, + "vertices": 66, + "wall_ms": 1.932146 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 84, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.741857, + "renderer_cpu_ms": 1.73865, + "renderer_readback_cpu_ms": 0.446142, + "simulation_ms": 0.149923, + "snapshot_ms": 0.142499, + "texture_count": 1, + "tick": 84, + "vertices": 66, + "wall_ms": 2.043035 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 85, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.681242, + "renderer_cpu_ms": 1.678888, + "renderer_readback_cpu_ms": 0.438357, + "simulation_ms": 0.157287, + "snapshot_ms": 0.145915, + "texture_count": 1, + "tick": 85, + "vertices": 66, + "wall_ms": 1.993141 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 86, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564608, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.009952, + "renderer_cpu_ms": 2.004141, + "renderer_readback_cpu_ms": 0.679502, + "simulation_ms": 0.168658, + "snapshot_ms": 0.146557, + "texture_count": 1, + "tick": 86, + "vertices": 66, + "wall_ms": 2.334324 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 87, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.574112, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.814344, + "renderer_cpu_ms": 1.811808, + "renderer_readback_cpu_ms": 0.432917, + "simulation_ms": 0.251595, + "snapshot_ms": 0.204105, + "texture_count": 1, + "tick": 87, + "vertices": 66, + "wall_ms": 2.279842 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 88, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564864, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.639864, + "renderer_cpu_ms": 1.637409, + "renderer_readback_cpu_ms": 0.483412, + "simulation_ms": 0.155303, + "snapshot_ms": 0.134634, + "texture_count": 1, + "tick": 88, + "vertices": 66, + "wall_ms": 1.938888 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 89, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565184, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.709075, + "renderer_cpu_ms": 1.705658, + "renderer_readback_cpu_ms": 0.454026, + "simulation_ms": 0.149102, + "snapshot_ms": 0.132019, + "texture_count": 1, + "tick": 89, + "vertices": 66, + "wall_ms": 1.999793 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 90, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.594338, + "renderer_cpu_ms": 1.592033, + "renderer_readback_cpu_ms": 0.45594, + "simulation_ms": 0.152689, + "snapshot_ms": 0.130957, + "texture_count": 1, + "tick": 90, + "vertices": 66, + "wall_ms": 1.888082 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 91, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564608, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.722439, + "renderer_cpu_ms": 1.720196, + "renderer_readback_cpu_ms": 0.458275, + "simulation_ms": 0.150734, + "snapshot_ms": 0.127741, + "texture_count": 1, + "tick": 91, + "vertices": 66, + "wall_ms": 2.010002 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 92, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56416, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.793754, + "renderer_cpu_ms": 1.7916, + "renderer_readback_cpu_ms": 0.452324, + "simulation_ms": 0.148099, + "snapshot_ms": 0.131659, + "texture_count": 1, + "tick": 92, + "vertices": 66, + "wall_ms": 2.08288 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 93, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564256, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.7704, + "renderer_cpu_ms": 1.766202, + "renderer_readback_cpu_ms": 0.599571, + "simulation_ms": 0.154131, + "snapshot_ms": 0.141086, + "texture_count": 1, + "tick": 93, + "vertices": 66, + "wall_ms": 2.074995 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 94, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564992, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.900856, + "renderer_cpu_ms": 1.896458, + "renderer_readback_cpu_ms": 0.479385, + "simulation_ms": 0.219074, + "snapshot_ms": 0.206078, + "texture_count": 1, + "tick": 94, + "vertices": 66, + "wall_ms": 2.33734 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 95, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563904, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.667256, + "renderer_cpu_ms": 1.66431, + "renderer_readback_cpu_ms": 0.480146, + "simulation_ms": 0.204225, + "snapshot_ms": 0.139724, + "texture_count": 1, + "tick": 95, + "vertices": 66, + "wall_ms": 2.037565 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 96, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565632, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.760502, + "renderer_cpu_ms": 1.758497, + "renderer_readback_cpu_ms": 0.486638, + "simulation_ms": 0.153019, + "snapshot_ms": 0.130226, + "texture_count": 1, + "tick": 96, + "vertices": 66, + "wall_ms": 2.053485 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 97, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.58192, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.799836, + "renderer_cpu_ms": 1.79686, + "renderer_readback_cpu_ms": 0.482951, + "simulation_ms": 0.152788, + "snapshot_ms": 0.129545, + "texture_count": 1, + "tick": 97, + "vertices": 66, + "wall_ms": 2.091045 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 98, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564992, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.718983, + "renderer_cpu_ms": 1.716579, + "renderer_readback_cpu_ms": 0.528437, + "simulation_ms": 0.153068, + "snapshot_ms": 0.136658, + "texture_count": 1, + "tick": 98, + "vertices": 66, + "wall_ms": 2.018328 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 99, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.04048, + "renderer_cpu_ms": 2.035771, + "renderer_readback_cpu_ms": 0.632894, + "simulation_ms": 0.154632, + "snapshot_ms": 0.134654, + "texture_count": 1, + "tick": 99, + "vertices": 66, + "wall_ms": 2.338291 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 100, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564288, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.715467, + "renderer_cpu_ms": 1.712942, + "renderer_readback_cpu_ms": 0.492078, + "simulation_ms": 0.184448, + "snapshot_ms": 0.194056, + "texture_count": 1, + "tick": 100, + "vertices": 66, + "wall_ms": 2.104491 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 101, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.968234, + "renderer_cpu_ms": 1.964346, + "renderer_readback_cpu_ms": 0.601074, + "simulation_ms": 0.169399, + "snapshot_ms": 0.135947, + "texture_count": 1, + "tick": 101, + "vertices": 66, + "wall_ms": 2.2836090000000002 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 102, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56496, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.836725, + "renderer_cpu_ms": 1.832687, + "renderer_readback_cpu_ms": 0.473123, + "simulation_ms": 0.206249, + "snapshot_ms": 0.158669, + "texture_count": 1, + "tick": 102, + "vertices": 66, + "wall_ms": 2.214378 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 103, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.77531, + "renderer_cpu_ms": 1.771161, + "renderer_readback_cpu_ms": 0.559807, + "simulation_ms": 0.157577, + "snapshot_ms": 0.136037, + "texture_count": 1, + "tick": 103, + "vertices": 66, + "wall_ms": 2.079243 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 104, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.567936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.748108, + "renderer_cpu_ms": 1.745173, + "renderer_readback_cpu_ms": 0.459768, + "simulation_ms": 0.154872, + "snapshot_ms": 0.134204, + "texture_count": 1, + "tick": 104, + "vertices": 66, + "wall_ms": 2.047623 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 105, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.572352, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.773285, + "renderer_cpu_ms": 1.770501, + "renderer_readback_cpu_ms": 0.459026, + "simulation_ms": 0.160833, + "snapshot_ms": 0.130977, + "texture_count": 1, + "tick": 105, + "vertices": 66, + "wall_ms": 2.073592 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 106, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.661716, + "renderer_cpu_ms": 1.65921, + "renderer_readback_cpu_ms": 0.483372, + "simulation_ms": 0.164891, + "snapshot_ms": 0.168348, + "texture_count": 1, + "tick": 106, + "vertices": 66, + "wall_ms": 2.005083 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 107, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56528, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.866331, + "renderer_cpu_ms": 1.863125, + "renderer_readback_cpu_ms": 0.512367, + "simulation_ms": 0.157728, + "snapshot_ms": 0.152427, + "texture_count": 1, + "tick": 107, + "vertices": 66, + "wall_ms": 2.185503 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 108, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563616, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.9344, + "renderer_cpu_ms": 1.930442, + "renderer_readback_cpu_ms": 0.515062, + "simulation_ms": 0.227519, + "snapshot_ms": 0.184799, + "texture_count": 1, + "tick": 108, + "vertices": 66, + "wall_ms": 2.355214 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 109, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56432, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.762225, + "renderer_cpu_ms": 1.756564, + "renderer_readback_cpu_ms": 0.54068, + "simulation_ms": 0.15905, + "snapshot_ms": 0.137049, + "texture_count": 1, + "tick": 109, + "vertices": 66, + "wall_ms": 2.067882 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 110, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.947434, + "renderer_cpu_ms": 1.944479, + "renderer_readback_cpu_ms": 0.569425, + "simulation_ms": 0.184899, + "snapshot_ms": 0.152568, + "texture_count": 1, + "tick": 110, + "vertices": 66, + "wall_ms": 2.29512 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 111, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.572032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.123677, + "renderer_cpu_ms": 2.118758, + "renderer_readback_cpu_ms": 0.509862, + "simulation_ms": 0.227489, + "snapshot_ms": 0.157417, + "texture_count": 1, + "tick": 111, + "vertices": 66, + "wall_ms": 2.521518 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 112, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564352, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.794405, + "renderer_cpu_ms": 1.789737, + "renderer_readback_cpu_ms": 0.574935, + "simulation_ms": 0.162827, + "snapshot_ms": 0.132641, + "texture_count": 1, + "tick": 112, + "vertices": 66, + "wall_ms": 2.101955 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 113, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.990525, + "renderer_cpu_ms": 1.985747, + "renderer_readback_cpu_ms": 0.586727, + "simulation_ms": 0.157988, + "snapshot_ms": 0.133412, + "texture_count": 1, + "tick": 113, + "vertices": 66, + "wall_ms": 2.292144 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 114, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573824, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.91858, + "renderer_cpu_ms": 1.914121, + "renderer_readback_cpu_ms": 0.515773, + "simulation_ms": 0.243028, + "snapshot_ms": 0.188376, + "texture_count": 1, + "tick": 114, + "vertices": 66, + "wall_ms": 2.360083 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 115, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.134798, + "renderer_cpu_ms": 2.130439, + "renderer_readback_cpu_ms": 0.542934, + "simulation_ms": 0.258247, + "snapshot_ms": 0.220416, + "texture_count": 1, + "tick": 115, + "vertices": 66, + "wall_ms": 2.624141 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 116, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.571776, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.041702, + "renderer_cpu_ms": 2.036752, + "renderer_readback_cpu_ms": 0.57783, + "simulation_ms": 0.29166, + "snapshot_ms": 0.228091, + "texture_count": 1, + "tick": 116, + "vertices": 66, + "wall_ms": 2.573375 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 117, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.11999, + "renderer_cpu_ms": 2.113618, + "renderer_readback_cpu_ms": 0.623586, + "simulation_ms": 0.267184, + "snapshot_ms": 0.24958, + "texture_count": 1, + "tick": 117, + "vertices": 66, + "wall_ms": 2.647024 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 118, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565664, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.467716, + "renderer_cpu_ms": 2.463788, + "renderer_readback_cpu_ms": 0.690243, + "simulation_ms": 0.285859, + "snapshot_ms": 0.230495, + "texture_count": 1, + "tick": 118, + "vertices": 66, + "wall_ms": 2.995241 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 119, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.88169, + "renderer_cpu_ms": 1.877663, + "renderer_readback_cpu_ms": 0.543476, + "simulation_ms": 0.252396, + "snapshot_ms": 0.196942, + "texture_count": 1, + "tick": 119, + "vertices": 66, + "wall_ms": 2.341508 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 120, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.566368, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.007457, + "renderer_cpu_ms": 2.002168, + "renderer_readback_cpu_ms": 0.584122, + "simulation_ms": 0.213914, + "snapshot_ms": 0.213903, + "texture_count": 1, + "tick": 120, + "vertices": 66, + "wall_ms": 2.444802 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 121, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.991557, + "renderer_cpu_ms": 1.98811, + "renderer_readback_cpu_ms": 0.549306, + "simulation_ms": 0.228732, + "snapshot_ms": 0.184799, + "texture_count": 1, + "tick": 121, + "vertices": 66, + "wall_ms": 2.415086 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 122, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.90272, + "renderer_cpu_ms": 1.897891, + "renderer_readback_cpu_ms": 0.530871, + "simulation_ms": 0.209856, + "snapshot_ms": 0.202823, + "texture_count": 1, + "tick": 122, + "vertices": 66, + "wall_ms": 2.325377 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 123, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.896879, + "renderer_cpu_ms": 1.891649, + "renderer_readback_cpu_ms": 0.605523, + "simulation_ms": 0.244251, + "snapshot_ms": 0.192573, + "texture_count": 1, + "tick": 123, + "vertices": 66, + "wall_ms": 2.344013 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 124, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.053254, + "renderer_cpu_ms": 2.048134, + "renderer_readback_cpu_ms": 0.578572, + "simulation_ms": 0.223702, + "snapshot_ms": 0.191402, + "texture_count": 1, + "tick": 124, + "vertices": 66, + "wall_ms": 2.481772 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 125, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.211112, + "renderer_cpu_ms": 2.206002, + "renderer_readback_cpu_ms": 0.642582, + "simulation_ms": 0.392911, + "snapshot_ms": 0.25444, + "texture_count": 1, + "tick": 125, + "vertices": 66, + "wall_ms": 2.868833 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 126, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564128, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.904493, + "renderer_cpu_ms": 1.900475, + "renderer_readback_cpu_ms": 0.58831, + "simulation_ms": 0.247397, + "snapshot_ms": 0.192854, + "texture_count": 1, + "tick": 126, + "vertices": 66, + "wall_ms": 2.355384 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 127, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.9928, + "renderer_cpu_ms": 1.989233, + "renderer_readback_cpu_ms": 0.566749, + "simulation_ms": 0.216298, + "snapshot_ms": 0.187494, + "texture_count": 1, + "tick": 127, + "vertices": 66, + "wall_ms": 2.407472 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 128, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.107998, + "renderer_cpu_ms": 2.102267, + "renderer_readback_cpu_ms": 0.644586, + "simulation_ms": 0.218292, + "snapshot_ms": 0.210778, + "texture_count": 1, + "tick": 128, + "vertices": 66, + "wall_ms": 2.547757 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 129, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57552, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.954477, + "renderer_cpu_ms": 1.951252, + "renderer_readback_cpu_ms": 0.474445, + "simulation_ms": 0.235114, + "snapshot_ms": 0.203924, + "texture_count": 1, + "tick": 129, + "vertices": 66, + "wall_ms": 2.405388 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 130, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.022195, + "renderer_cpu_ms": 2.017797, + "renderer_readback_cpu_ms": 0.634016, + "simulation_ms": 0.229924, + "snapshot_ms": 0.210076, + "texture_count": 1, + "tick": 130, + "vertices": 66, + "wall_ms": 2.471403 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 131, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565408, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.124739, + "renderer_cpu_ms": 2.119869, + "renderer_readback_cpu_ms": 0.582469, + "simulation_ms": 0.231086, + "snapshot_ms": 0.201319, + "texture_count": 1, + "tick": 131, + "vertices": 66, + "wall_ms": 2.568086 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 132, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564928, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.697029, + "renderer_cpu_ms": 2.690446, + "renderer_readback_cpu_ms": 0.695682, + "simulation_ms": 0.245914, + "snapshot_ms": 0.190339, + "texture_count": 1, + "tick": 132, + "vertices": 66, + "wall_ms": 3.143671 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 133, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.226401, + "renderer_cpu_ms": 2.222283, + "renderer_readback_cpu_ms": 0.624128, + "simulation_ms": 0.241866, + "snapshot_ms": 0.20151, + "texture_count": 1, + "tick": 133, + "vertices": 66, + "wall_ms": 2.684014 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 134, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.123717, + "renderer_cpu_ms": 2.120791, + "renderer_readback_cpu_ms": 0.476609, + "simulation_ms": 0.264369, + "snapshot_ms": 0.210036, + "texture_count": 1, + "tick": 134, + "vertices": 66, + "wall_ms": 2.610034 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 135, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.787181, + "renderer_cpu_ms": 1.783595, + "renderer_readback_cpu_ms": 0.564254, + "simulation_ms": 0.166955, + "snapshot_ms": 0.139894, + "texture_count": 1, + "tick": 135, + "vertices": 66, + "wall_ms": 2.103719 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 136, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564864, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.818912, + "renderer_cpu_ms": 1.815595, + "renderer_readback_cpu_ms": 0.447965, + "simulation_ms": 0.161635, + "snapshot_ms": 0.133953, + "texture_count": 1, + "tick": 136, + "vertices": 66, + "wall_ms": 2.124037 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 137, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.674268, + "renderer_cpu_ms": 1.670872, + "renderer_readback_cpu_ms": 0.504933, + "simulation_ms": 0.159572, + "snapshot_ms": 0.15392, + "texture_count": 1, + "tick": 137, + "vertices": 66, + "wall_ms": 1.997488 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 138, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.830213, + "renderer_cpu_ms": 1.826597, + "renderer_readback_cpu_ms": 0.483692, + "simulation_ms": 0.15337, + "snapshot_ms": 0.154571, + "texture_count": 1, + "tick": 138, + "vertices": 66, + "wall_ms": 2.148123 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 139, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.570688, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.76456, + "renderer_cpu_ms": 1.762135, + "renderer_readback_cpu_ms": 0.459036, + "simulation_ms": 0.181151, + "snapshot_ms": 0.15357, + "texture_count": 1, + "tick": 139, + "vertices": 66, + "wall_ms": 2.111113 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 140, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.612342, + "renderer_cpu_ms": 1.608776, + "renderer_readback_cpu_ms": 0.499011, + "simulation_ms": 0.144743, + "snapshot_ms": 0.148069, + "texture_count": 1, + "tick": 140, + "vertices": 66, + "wall_ms": 1.912929 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 141, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.793684, + "renderer_cpu_ms": 1.790878, + "renderer_readback_cpu_ms": 0.494493, + "simulation_ms": 0.150634, + "snapshot_ms": 0.150274, + "texture_count": 1, + "tick": 141, + "vertices": 66, + "wall_ms": 2.104922 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 142, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.741225, + "renderer_cpu_ms": 1.738069, + "renderer_readback_cpu_ms": 0.456402, + "simulation_ms": 0.182965, + "snapshot_ms": 0.146566, + "texture_count": 1, + "tick": 142, + "vertices": 66, + "wall_ms": 2.081848 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 143, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.810466, + "renderer_cpu_ms": 1.805877, + "renderer_readback_cpu_ms": 0.524249, + "simulation_ms": 0.151205, + "snapshot_ms": 0.144423, + "texture_count": 1, + "tick": 143, + "vertices": 66, + "wall_ms": 2.115231 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 144, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.574336, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.855281, + "renderer_cpu_ms": 1.852355, + "renderer_readback_cpu_ms": 0.49831, + "simulation_ms": 0.156234, + "snapshot_ms": 0.133913, + "texture_count": 1, + "tick": 144, + "vertices": 66, + "wall_ms": 2.155076 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 145, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563904, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.734864, + "renderer_cpu_ms": 1.732358, + "renderer_readback_cpu_ms": 0.481609, + "simulation_ms": 0.156896, + "snapshot_ms": 0.13789, + "texture_count": 1, + "tick": 145, + "vertices": 66, + "wall_ms": 2.039178 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 146, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.938808, + "renderer_cpu_ms": 1.933958, + "renderer_readback_cpu_ms": 0.494603, + "simulation_ms": 0.152858, + "snapshot_ms": 0.132751, + "texture_count": 1, + "tick": 146, + "vertices": 66, + "wall_ms": 2.233955 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 147, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.586272, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.905295, + "renderer_cpu_ms": 1.902469, + "renderer_readback_cpu_ms": 0.48222, + "simulation_ms": 0.174229, + "snapshot_ms": 0.131628, + "texture_count": 1, + "tick": 147, + "vertices": 66, + "wall_ms": 2.221491 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 148, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.652448, + "renderer_cpu_ms": 1.649973, + "renderer_readback_cpu_ms": 0.512097, + "simulation_ms": 0.152187, + "snapshot_ms": 0.129585, + "texture_count": 1, + "tick": 148, + "vertices": 66, + "wall_ms": 1.942906 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 149, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.835813, + "renderer_cpu_ms": 1.831125, + "renderer_readback_cpu_ms": 0.507978, + "simulation_ms": 0.155994, + "snapshot_ms": 0.131608, + "texture_count": 1, + "tick": 149, + "vertices": 66, + "wall_ms": 2.133485 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 150, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.747306, + "renderer_cpu_ms": 1.744141, + "renderer_readback_cpu_ms": 0.490636, + "simulation_ms": 0.172986, + "snapshot_ms": 0.146436, + "texture_count": 1, + "tick": 150, + "vertices": 66, + "wall_ms": 2.07777 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 151, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.569568, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.864167, + "renderer_cpu_ms": 1.860731, + "renderer_readback_cpu_ms": 0.527465, + "simulation_ms": 0.16949, + "snapshot_ms": 0.143721, + "texture_count": 1, + "tick": 151, + "vertices": 66, + "wall_ms": 2.188109 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 152, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.575168, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.855712, + "renderer_cpu_ms": 1.851644, + "renderer_readback_cpu_ms": 0.485796, + "simulation_ms": 0.176483, + "snapshot_ms": 0.158579, + "texture_count": 1, + "tick": 152, + "vertices": 66, + "wall_ms": 2.203949 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 153, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56416, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.674769, + "renderer_cpu_ms": 1.672305, + "renderer_readback_cpu_ms": 0.466079, + "simulation_ms": 0.172445, + "snapshot_ms": 0.143581, + "texture_count": 1, + "tick": 153, + "vertices": 66, + "wall_ms": 2.00349 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 154, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564896, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.818381, + "renderer_cpu_ms": 1.814855, + "renderer_readback_cpu_ms": 0.515522, + "simulation_ms": 0.171223, + "snapshot_ms": 0.14303, + "texture_count": 1, + "tick": 154, + "vertices": 66, + "wall_ms": 2.141841 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 155, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.879226, + "renderer_cpu_ms": 1.875799, + "renderer_readback_cpu_ms": 0.53567, + "simulation_ms": 0.180841, + "snapshot_ms": 0.135896, + "texture_count": 1, + "tick": 155, + "vertices": 66, + "wall_ms": 2.206273 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 156, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.000114, + "renderer_cpu_ms": 1.996096, + "renderer_readback_cpu_ms": 0.56731, + "simulation_ms": 0.159631, + "snapshot_ms": 0.134694, + "texture_count": 1, + "tick": 156, + "vertices": 66, + "wall_ms": 2.304318 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 157, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564864, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.034278, + "renderer_cpu_ms": 2.02987, + "renderer_readback_cpu_ms": 0.513479, + "simulation_ms": 0.19584, + "snapshot_ms": 0.203674, + "texture_count": 1, + "tick": 157, + "vertices": 66, + "wall_ms": 2.446355 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 158, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.585536, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.035851, + "renderer_cpu_ms": 2.031783, + "renderer_readback_cpu_ms": 0.495956, + "simulation_ms": 0.232739, + "snapshot_ms": 0.17488, + "texture_count": 1, + "tick": 158, + "vertices": 66, + "wall_ms": 2.454721 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 159, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.704456, + "renderer_cpu_ms": 1.70141, + "renderer_readback_cpu_ms": 0.478874, + "simulation_ms": 0.165723, + "snapshot_ms": 0.168358, + "texture_count": 1, + "tick": 159, + "vertices": 66, + "wall_ms": 2.048986 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 160, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.865269, + "renderer_cpu_ms": 1.861723, + "renderer_readback_cpu_ms": 0.50885, + "simulation_ms": 0.156395, + "snapshot_ms": 0.165011, + "texture_count": 1, + "tick": 160, + "vertices": 66, + "wall_ms": 2.197145 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 161, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.820505, + "renderer_cpu_ms": 1.817159, + "renderer_readback_cpu_ms": 0.49303, + "simulation_ms": 0.176894, + "snapshot_ms": 0.145674, + "texture_count": 1, + "tick": 161, + "vertices": 66, + "wall_ms": 2.167991 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 162, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564288, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.718452, + "renderer_cpu_ms": 1.714695, + "renderer_readback_cpu_ms": 0.517206, + "simulation_ms": 0.178096, + "snapshot_ms": 0.140626, + "texture_count": 1, + "tick": 162, + "vertices": 66, + "wall_ms": 2.046811 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 163, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.904193, + "renderer_cpu_ms": 1.900195, + "renderer_readback_cpu_ms": 0.519319, + "simulation_ms": 0.176764, + "snapshot_ms": 0.142088, + "texture_count": 1, + "tick": 163, + "vertices": 66, + "wall_ms": 2.234606 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 164, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.83391, + "renderer_cpu_ms": 1.830343, + "renderer_readback_cpu_ms": 0.520331, + "simulation_ms": 0.184749, + "snapshot_ms": 0.162867, + "texture_count": 1, + "tick": 164, + "vertices": 66, + "wall_ms": 2.191144 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 165, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.86571, + "renderer_cpu_ms": 1.862524, + "renderer_readback_cpu_ms": 0.525221, + "simulation_ms": 0.189899, + "snapshot_ms": 0.201069, + "texture_count": 1, + "tick": 165, + "vertices": 66, + "wall_ms": 2.267357 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 166, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.579712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.92931, + "renderer_cpu_ms": 1.925423, + "renderer_readback_cpu_ms": 0.511975, + "simulation_ms": 0.168659, + "snapshot_ms": 0.150323, + "texture_count": 1, + "tick": 166, + "vertices": 66, + "wall_ms": 2.257971 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 167, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.822409, + "renderer_cpu_ms": 1.818481, + "renderer_readback_cpu_ms": 0.552252, + "simulation_ms": 0.181983, + "snapshot_ms": 0.157237, + "texture_count": 1, + "tick": 167, + "vertices": 66, + "wall_ms": 2.17296 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 168, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.011264, + "renderer_cpu_ms": 2.005744, + "renderer_readback_cpu_ms": 0.585836, + "simulation_ms": 0.16978, + "snapshot_ms": 0.139112, + "texture_count": 1, + "tick": 168, + "vertices": 66, + "wall_ms": 2.331519 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 169, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.387585, + "renderer_cpu_ms": 2.382866, + "renderer_readback_cpu_ms": 0.761547, + "simulation_ms": 0.250382, + "snapshot_ms": 0.205839, + "texture_count": 1, + "tick": 169, + "vertices": 66, + "wall_ms": 2.85656 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 170, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57856, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.399137, + "renderer_cpu_ms": 2.394117, + "renderer_readback_cpu_ms": 0.557693, + "simulation_ms": 0.22777, + "snapshot_ms": 0.197943, + "texture_count": 1, + "tick": 170, + "vertices": 66, + "wall_ms": 2.838045 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 171, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.894434, + "renderer_cpu_ms": 1.890968, + "renderer_readback_cpu_ms": 0.594021, + "simulation_ms": 0.225836, + "snapshot_ms": 0.209866, + "texture_count": 1, + "tick": 171, + "vertices": 66, + "wall_ms": 2.342199 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 172, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564608, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.14131, + "renderer_cpu_ms": 2.136681, + "renderer_readback_cpu_ms": 0.612977, + "simulation_ms": 0.215046, + "snapshot_ms": 0.192683, + "texture_count": 1, + "tick": 172, + "vertices": 66, + "wall_ms": 2.558467 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 173, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.041191, + "renderer_cpu_ms": 2.036482, + "renderer_readback_cpu_ms": 0.555679, + "simulation_ms": 0.222741, + "snapshot_ms": 0.205998, + "texture_count": 1, + "tick": 173, + "vertices": 66, + "wall_ms": 2.479618 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 174, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.57008, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.190423, + "renderer_cpu_ms": 2.185603, + "renderer_readback_cpu_ms": 0.553274, + "simulation_ms": 0.216699, + "snapshot_ms": 0.195369, + "texture_count": 1, + "tick": 174, + "vertices": 66, + "wall_ms": 2.612399 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 175, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.936814, + "renderer_cpu_ms": 1.932055, + "renderer_readback_cpu_ms": 0.586757, + "simulation_ms": 0.244922, + "snapshot_ms": 0.187383, + "texture_count": 1, + "tick": 175, + "vertices": 66, + "wall_ms": 2.37956 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 176, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565152, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.039468, + "renderer_cpu_ms": 2.034318, + "renderer_readback_cpu_ms": 0.545208, + "simulation_ms": 0.226567, + "snapshot_ms": 0.186362, + "texture_count": 1, + "tick": 176, + "vertices": 66, + "wall_ms": 2.462977 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 177, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.993872, + "renderer_cpu_ms": 1.989294, + "renderer_readback_cpu_ms": 0.540781, + "simulation_ms": 0.238129, + "snapshot_ms": 0.188957, + "texture_count": 1, + "tick": 177, + "vertices": 66, + "wall_ms": 2.430516 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 178, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.933968, + "renderer_cpu_ms": 1.92921, + "renderer_readback_cpu_ms": 0.57784, + "simulation_ms": 0.230575, + "snapshot_ms": 0.195579, + "texture_count": 1, + "tick": 178, + "vertices": 66, + "wall_ms": 2.371324 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 179, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565824, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.115141, + "renderer_cpu_ms": 2.110683, + "renderer_readback_cpu_ms": 0.601705, + "simulation_ms": 0.221788, + "snapshot_ms": 0.214024, + "texture_count": 1, + "tick": 179, + "vertices": 66, + "wall_ms": 2.561533 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 180, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.061079, + "renderer_cpu_ms": 2.05648, + "renderer_readback_cpu_ms": 0.56724, + "simulation_ms": 0.218091, + "snapshot_ms": 0.193826, + "texture_count": 1, + "tick": 180, + "vertices": 66, + "wall_ms": 2.484828 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 181, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.569312, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.016043, + "renderer_cpu_ms": 2.011074, + "renderer_readback_cpu_ms": 0.551891, + "simulation_ms": 0.237889, + "snapshot_ms": 0.195008, + "texture_count": 1, + "tick": 181, + "vertices": 66, + "wall_ms": 2.458809 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 182, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564192, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.01432, + "renderer_cpu_ms": 2.009512, + "renderer_readback_cpu_ms": 0.59919, + "simulation_ms": 0.216318, + "snapshot_ms": 0.183546, + "texture_count": 1, + "tick": 182, + "vertices": 66, + "wall_ms": 2.424905 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 183, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.566304, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.044067, + "renderer_cpu_ms": 2.039128, + "renderer_readback_cpu_ms": 0.573712, + "simulation_ms": 0.240885, + "snapshot_ms": 0.18527, + "texture_count": 1, + "tick": 183, + "vertices": 66, + "wall_ms": 2.48044 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 184, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.020772, + "renderer_cpu_ms": 2.015262, + "renderer_readback_cpu_ms": 0.579763, + "simulation_ms": 0.216378, + "snapshot_ms": 0.187794, + "texture_count": 1, + "tick": 184, + "vertices": 66, + "wall_ms": 2.435485 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 185, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.584032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.186646, + "renderer_cpu_ms": 2.183058, + "renderer_readback_cpu_ms": 0.504732, + "simulation_ms": 0.241516, + "snapshot_ms": 0.186101, + "texture_count": 1, + "tick": 185, + "vertices": 66, + "wall_ms": 2.625434 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 186, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.767706, + "renderer_cpu_ms": 1.763728, + "renderer_readback_cpu_ms": 0.510674, + "simulation_ms": 0.16993, + "snapshot_ms": 0.149182, + "texture_count": 1, + "tick": 186, + "vertices": 66, + "wall_ms": 2.096626 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 187, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564896, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.819429, + "renderer_cpu_ms": 2.814059, + "renderer_readback_cpu_ms": 0.591005, + "simulation_ms": 0.161995, + "snapshot_ms": 0.142579, + "texture_count": 1, + "tick": 187, + "vertices": 66, + "wall_ms": 3.133141 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 188, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.076718, + "renderer_cpu_ms": 2.072711, + "renderer_readback_cpu_ms": 0.586486, + "simulation_ms": 0.290047, + "snapshot_ms": 0.212531, + "texture_count": 1, + "tick": 188, + "vertices": 66, + "wall_ms": 2.590558 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 189, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.569216, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.015082, + "renderer_cpu_ms": 2.011165, + "renderer_readback_cpu_ms": 0.53542, + "simulation_ms": 0.230675, + "snapshot_ms": 0.206369, + "texture_count": 1, + "tick": 189, + "vertices": 66, + "wall_ms": 2.463749 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 190, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.002098, + "renderer_cpu_ms": 1.996517, + "renderer_readback_cpu_ms": 0.631211, + "simulation_ms": 0.257065, + "snapshot_ms": 0.200358, + "texture_count": 1, + "tick": 190, + "vertices": 66, + "wall_ms": 2.470842 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 191, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.568832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.110943, + "renderer_cpu_ms": 2.106083, + "renderer_readback_cpu_ms": 0.613828, + "simulation_ms": 0.236757, + "snapshot_ms": 0.210347, + "texture_count": 1, + "tick": 191, + "vertices": 66, + "wall_ms": 2.57082 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 192, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.22086, + "renderer_cpu_ms": 2.216031, + "renderer_readback_cpu_ms": 0.536753, + "simulation_ms": 0.243349, + "snapshot_ms": 0.202372, + "texture_count": 1, + "tick": 192, + "vertices": 66, + "wall_ms": 2.680057 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 193, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573696, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.227783, + "renderer_cpu_ms": 2.222874, + "renderer_readback_cpu_ms": 0.557301, + "simulation_ms": 0.262775, + "snapshot_ms": 0.20673, + "texture_count": 1, + "tick": 193, + "vertices": 66, + "wall_ms": 2.711806 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 194, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.211613, + "renderer_cpu_ms": 2.206012, + "renderer_readback_cpu_ms": 0.873999, + "simulation_ms": 0.225876, + "snapshot_ms": 0.218092, + "texture_count": 1, + "tick": 194, + "vertices": 66, + "wall_ms": 2.666401 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 195, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56544, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.157471, + "renderer_cpu_ms": 2.15208, + "renderer_readback_cpu_ms": 0.618517, + "simulation_ms": 0.23319, + "snapshot_ms": 0.20665, + "texture_count": 1, + "tick": 195, + "vertices": 66, + "wall_ms": 2.60764 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 196, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.086226, + "renderer_cpu_ms": 2.081387, + "renderer_readback_cpu_ms": 0.583892, + "simulation_ms": 0.296259, + "snapshot_ms": 0.210667, + "texture_count": 1, + "tick": 196, + "vertices": 66, + "wall_ms": 2.606047 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 197, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.577152, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.165656, + "renderer_cpu_ms": 2.161208, + "renderer_readback_cpu_ms": 0.636882, + "simulation_ms": 0.23813, + "snapshot_ms": 0.226086, + "texture_count": 1, + "tick": 197, + "vertices": 66, + "wall_ms": 2.641133 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 198, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56416, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.978914, + "renderer_cpu_ms": 1.973283, + "renderer_readback_cpu_ms": 0.590354, + "simulation_ms": 0.238791, + "snapshot_ms": 0.210136, + "texture_count": 1, + "tick": 198, + "vertices": 66, + "wall_ms": 2.439002 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 199, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565632, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.059095, + "renderer_cpu_ms": 2.054857, + "renderer_readback_cpu_ms": 0.585986, + "simulation_ms": 0.24898, + "snapshot_ms": 0.201981, + "texture_count": 1, + "tick": 199, + "vertices": 66, + "wall_ms": 2.523341 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 200, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.884055, + "renderer_cpu_ms": 1.879446, + "renderer_readback_cpu_ms": 0.492159, + "simulation_ms": 0.200799, + "snapshot_ms": 0.195348, + "texture_count": 1, + "tick": 200, + "vertices": 66, + "wall_ms": 2.290372 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 201, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56368, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.842206, + "renderer_cpu_ms": 1.838509, + "renderer_readback_cpu_ms": 0.53031, + "simulation_ms": 0.222159, + "snapshot_ms": 0.22268, + "texture_count": 1, + "tick": 201, + "vertices": 66, + "wall_ms": 2.299919 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 202, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564928, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 5.705444, + "renderer_cpu_ms": 5.701056, + "renderer_readback_cpu_ms": 0.574985, + "simulation_ms": 0.191451, + "snapshot_ms": 0.163279, + "texture_count": 1, + "tick": 202, + "vertices": 66, + "wall_ms": 6.089358 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 203, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.571264, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.102647, + "renderer_cpu_ms": 2.097287, + "renderer_readback_cpu_ms": 0.56207, + "simulation_ms": 0.242458, + "snapshot_ms": 0.207932, + "texture_count": 1, + "tick": 203, + "vertices": 66, + "wall_ms": 2.563115 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 204, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.290441, + "renderer_cpu_ms": 2.285552, + "renderer_readback_cpu_ms": 0.569023, + "simulation_ms": 0.241015, + "snapshot_ms": 0.227439, + "texture_count": 1, + "tick": 204, + "vertices": 66, + "wall_ms": 2.770166 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 205, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.8979, + "renderer_cpu_ms": 1.893262, + "renderer_readback_cpu_ms": 0.587158, + "simulation_ms": 0.239452, + "snapshot_ms": 0.214344, + "texture_count": 1, + "tick": 205, + "vertices": 66, + "wall_ms": 2.361866 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 206, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.570336, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.978042, + "renderer_cpu_ms": 1.973583, + "renderer_readback_cpu_ms": 0.511956, + "simulation_ms": 0.305877, + "snapshot_ms": 0.203354, + "texture_count": 1, + "tick": 206, + "vertices": 66, + "wall_ms": 2.497522 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 207, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.946713, + "renderer_cpu_ms": 1.941302, + "renderer_readback_cpu_ms": 0.561399, + "simulation_ms": 0.513789, + "snapshot_ms": 0.184468, + "texture_count": 1, + "tick": 207, + "vertices": 66, + "wall_ms": 2.65555 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 208, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.943437, + "renderer_cpu_ms": 1.939319, + "renderer_readback_cpu_ms": 0.520822, + "simulation_ms": 0.229082, + "snapshot_ms": 0.183466, + "texture_count": 1, + "tick": 208, + "vertices": 66, + "wall_ms": 2.365874 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 209, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564896, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.41129, + "renderer_cpu_ms": 2.408254, + "renderer_readback_cpu_ms": 1.25096, + "simulation_ms": 0.226307, + "snapshot_ms": 0.198845, + "texture_count": 1, + "tick": 209, + "vertices": 66, + "wall_ms": 2.847252 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 210, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565344, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.846614, + "renderer_cpu_ms": 1.843618, + "renderer_readback_cpu_ms": 0.501717, + "simulation_ms": 0.156906, + "snapshot_ms": 0.139774, + "texture_count": 1, + "tick": 210, + "vertices": 66, + "wall_ms": 2.153353 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 211, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.575456, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.767094, + "renderer_cpu_ms": 1.764178, + "renderer_readback_cpu_ms": 0.509291, + "simulation_ms": 0.154141, + "snapshot_ms": 0.135195, + "texture_count": 1, + "tick": 211, + "vertices": 66, + "wall_ms": 2.065877 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 212, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.735073, + "renderer_cpu_ms": 1.731397, + "renderer_readback_cpu_ms": 0.557471, + "simulation_ms": 0.162056, + "snapshot_ms": 0.159901, + "texture_count": 1, + "tick": 212, + "vertices": 66, + "wall_ms": 2.066829 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 213, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.927317, + "renderer_cpu_ms": 1.92408, + "renderer_readback_cpu_ms": 0.544787, + "simulation_ms": 0.231937, + "snapshot_ms": 0.183717, + "texture_count": 1, + "tick": 213, + "vertices": 66, + "wall_ms": 2.351838 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 214, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.895316, + "renderer_cpu_ms": 1.892631, + "renderer_readback_cpu_ms": 0.537103, + "simulation_ms": 0.215476, + "snapshot_ms": 0.194197, + "texture_count": 1, + "tick": 214, + "vertices": 66, + "wall_ms": 2.315168 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 215, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.773095, + "renderer_cpu_ms": 1.770139, + "renderer_readback_cpu_ms": 0.554166, + "simulation_ms": 0.20121, + "snapshot_ms": 0.170782, + "texture_count": 1, + "tick": 215, + "vertices": 66, + "wall_ms": 2.154204 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 216, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.882942, + "renderer_cpu_ms": 1.879677, + "renderer_readback_cpu_ms": 0.562972, + "simulation_ms": 0.214074, + "snapshot_ms": 0.168809, + "texture_count": 1, + "tick": 216, + "vertices": 66, + "wall_ms": 2.274732 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 217, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565216, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.837987, + "renderer_cpu_ms": 1.834942, + "renderer_readback_cpu_ms": 0.518679, + "simulation_ms": 0.193014, + "snapshot_ms": 0.173317, + "texture_count": 1, + "tick": 217, + "vertices": 66, + "wall_ms": 2.213977 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 218, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.738289, + "renderer_cpu_ms": 1.735775, + "renderer_readback_cpu_ms": 0.550479, + "simulation_ms": 0.197853, + "snapshot_ms": 0.172034, + "texture_count": 1, + "tick": 218, + "vertices": 66, + "wall_ms": 2.116964 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 219, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.896638, + "renderer_cpu_ms": 1.894134, + "renderer_readback_cpu_ms": 0.543215, + "simulation_ms": 0.19603, + "snapshot_ms": 0.162938, + "texture_count": 1, + "tick": 219, + "vertices": 66, + "wall_ms": 2.263971 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 220, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.828851, + "renderer_cpu_ms": 1.826115, + "renderer_readback_cpu_ms": 0.500024, + "simulation_ms": 0.212551, + "snapshot_ms": 0.193215, + "texture_count": 1, + "tick": 220, + "vertices": 66, + "wall_ms": 2.243152 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 221, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564192, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.581003, + "renderer_cpu_ms": 1.578639, + "renderer_readback_cpu_ms": 0.453365, + "simulation_ms": 0.159151, + "snapshot_ms": 0.131297, + "texture_count": 1, + "tick": 221, + "vertices": 66, + "wall_ms": 1.879436 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 222, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.729063, + "renderer_cpu_ms": 1.726878, + "renderer_readback_cpu_ms": 0.477681, + "simulation_ms": 0.166935, + "snapshot_ms": 0.129494, + "texture_count": 1, + "tick": 222, + "vertices": 66, + "wall_ms": 2.033387 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 223, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.563968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.588637, + "renderer_cpu_ms": 1.586303, + "renderer_readback_cpu_ms": 0.480466, + "simulation_ms": 0.152188, + "snapshot_ms": 0.131047, + "texture_count": 1, + "tick": 223, + "vertices": 66, + "wall_ms": 1.882271 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 224, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.568352, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.700298, + "renderer_cpu_ms": 1.698073, + "renderer_readback_cpu_ms": 0.476359, + "simulation_ms": 0.164821, + "snapshot_ms": 0.129284, + "texture_count": 1, + "tick": 224, + "vertices": 66, + "wall_ms": 2.003039 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 225, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.573568, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.743299, + "renderer_cpu_ms": 1.741225, + "renderer_readback_cpu_ms": 0.505053, + "simulation_ms": 0.184117, + "snapshot_ms": 0.129535, + "texture_count": 1, + "tick": 225, + "vertices": 66, + "wall_ms": 2.065868 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 226, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.602834, + "renderer_cpu_ms": 1.600108, + "renderer_readback_cpu_ms": 0.484584, + "simulation_ms": 0.156556, + "snapshot_ms": 0.129935, + "texture_count": 1, + "tick": 226, + "vertices": 66, + "wall_ms": 1.898051 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 227, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.729182, + "renderer_cpu_ms": 1.726638, + "renderer_readback_cpu_ms": 0.469236, + "simulation_ms": 0.154331, + "snapshot_ms": 0.131748, + "texture_count": 1, + "tick": 227, + "vertices": 66, + "wall_ms": 2.025461 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 228, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.60091, + "renderer_cpu_ms": 1.598416, + "renderer_readback_cpu_ms": 0.478783, + "simulation_ms": 0.147508, + "snapshot_ms": 0.130146, + "texture_count": 1, + "tick": 228, + "vertices": 66, + "wall_ms": 1.887871 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 229, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.862414, + "renderer_cpu_ms": 1.858347, + "renderer_readback_cpu_ms": 0.559285, + "simulation_ms": 0.152027, + "snapshot_ms": 0.129605, + "texture_count": 1, + "tick": 229, + "vertices": 66, + "wall_ms": 2.170575 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 230, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.571104, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.938918, + "renderer_cpu_ms": 1.936343, + "renderer_readback_cpu_ms": 0.443377, + "simulation_ms": 0.223672, + "snapshot_ms": 0.175802, + "texture_count": 1, + "tick": 230, + "vertices": 66, + "wall_ms": 2.348591 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 231, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565184, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.652277, + "renderer_cpu_ms": 1.649913, + "renderer_readback_cpu_ms": 0.457202, + "simulation_ms": 0.147779, + "snapshot_ms": 0.1272, + "texture_count": 1, + "tick": 231, + "vertices": 66, + "wall_ms": 1.935802 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 232, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564512, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.733721, + "renderer_cpu_ms": 1.731567, + "renderer_readback_cpu_ms": 0.481789, + "simulation_ms": 0.15367, + "snapshot_ms": 0.138131, + "texture_count": 1, + "tick": 232, + "vertices": 66, + "wall_ms": 2.032926 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 233, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.660483, + "renderer_cpu_ms": 1.654742, + "renderer_readback_cpu_ms": 0.483642, + "simulation_ms": 0.156715, + "snapshot_ms": 0.135526, + "texture_count": 1, + "tick": 233, + "vertices": 66, + "wall_ms": 1.960639 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 234, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.56448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.788705, + "renderer_cpu_ms": 1.78574, + "renderer_readback_cpu_ms": 0.468514, + "simulation_ms": 0.201801, + "snapshot_ms": 0.137269, + "texture_count": 1, + "tick": 234, + "vertices": 66, + "wall_ms": 2.142733 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 235, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.569728, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.797561, + "renderer_cpu_ms": 1.793394, + "renderer_readback_cpu_ms": 0.473864, + "simulation_ms": 0.161144, + "snapshot_ms": 0.164109, + "texture_count": 1, + "tick": 235, + "vertices": 66, + "wall_ms": 2.131672 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 236, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.684188, + "renderer_cpu_ms": 1.681273, + "renderer_readback_cpu_ms": 0.494473, + "simulation_ms": 0.229152, + "snapshot_ms": 0.171935, + "texture_count": 1, + "tick": 236, + "vertices": 66, + "wall_ms": 2.095233 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 237, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.096456, + "renderer_cpu_ms": 2.091706, + "renderer_readback_cpu_ms": 0.749845, + "simulation_ms": 0.154691, + "snapshot_ms": 0.147038, + "texture_count": 1, + "tick": 237, + "vertices": 66, + "wall_ms": 2.408695 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 238, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.574432, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.967782, + "renderer_cpu_ms": 1.963875, + "renderer_readback_cpu_ms": 0.536001, + "simulation_ms": 0.245463, + "snapshot_ms": 0.197643, + "texture_count": 1, + "tick": 238, + "vertices": 66, + "wall_ms": 2.424494 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 239, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.565728, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.780088, + "renderer_cpu_ms": 1.777263, + "renderer_readback_cpu_ms": 0.475728, + "simulation_ms": 0.162186, + "snapshot_ms": 0.133482, + "texture_count": 1, + "tick": 239, + "vertices": 66, + "wall_ms": 2.107726 + }, + { + "draw_calls": 11, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 240, + "gpu_allocated_bytes": 15750288, + "gpu_ms": 0.564832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.734332, + "renderer_cpu_ms": 1.730114, + "renderer_readback_cpu_ms": 0.440721, + "simulation_ms": 0.159982, + "snapshot_ms": 0.16459, + "texture_count": 1, + "tick": 240, + "vertices": 66, + "wall_ms": 2.069976 + } + ], + "simulation_mode": "synthetic_fixed_timestep", + "startup_ms": { + "main_to_first_frame": 265.511128, + "renderer_initialization": 258.825585, + "scene_read": 0.41301, + "world_initialization": 0.502017 + }, + "summary_ms": { + "gpu": { + "max": 0.58912, + "min": 0.563616, + "p50": 0.564832, + "p95": 0.57856, + "samples": 240 + }, + "render_call": { + "max": 5.705444, + "min": 1.581003, + "p50": 1.865269, + "p95": 2.22086, + "samples": 240 + }, + "renderer_cpu": { + "max": 5.701056, + "min": 1.578639, + "p50": 1.861723, + "p95": 2.216031, + "samples": 240 + }, + "renderer_readback_cpu": { + "max": 2.763424, + "min": 0.432917, + "p50": 0.509912, + "p95": 0.632894, + "samples": 240 + }, + "simulation": { + "max": 0.513789, + "min": 0.144743, + "p50": 0.200708, + "p95": 0.258247, + "samples": 240 + }, + "snapshot": { + "max": 0.25444, + "min": 0.127029, + "p50": 0.170341, + "p95": 0.214946, + "samples": 240 + }, + "wall": { + "max": 6.089358, + "min": 1.879436, + "p50": 2.233955, + "p95": 2.684014, + "samples": 240 + } + }, + "timing_notes": "Durations use steady_clock wall time, not thread CPU usage. render_call/renderer_cpu include GPU waits, readback and window presentation. renderer_readback_cpu measures map/copy/unmap wall time within that call. GPU timestamps cover submitted rendering, not CPU work. A missing/zero GPU timestamp is null. Frame durations exclude profile bookkeeping and final capture/profile file writes. Startup begins at main(), excluding OS loader/launcher.", + "validation_enabled": true, + "validation_errors": 0, + "version": 1, + "width": 1280 +} diff --git a/docs/validation/linux-release-2026-09-18/collect-2d.png b/docs/validation/linux-release-2026-09-18/collect-2d.png new file mode 100644 index 0000000..49a4fa6 Binary files /dev/null and b/docs/validation/linux-release-2026-09-18/collect-2d.png differ diff --git a/docs/validation/linux-release-2026-09-18/collect-3d-manifest.json b/docs/validation/linux-release-2026-09-18/collect-3d-manifest.json new file mode 100644 index 0000000..b35678d --- /dev/null +++ b/docs/validation/linux-release-2026-09-18/collect-3d-manifest.json @@ -0,0 +1,170 @@ +{ + "asset_generations": { + "5832763b-3ed0-44d6-9088-0b524f196a91": "4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2" + }, + "build_fingerprint": "5a516f5c4bded8f8e3f027b3dd2812a34b7f40b37e281c21ce87d7586fff88e4", + "configuration": "Release", + "executable": "faset_player", + "files": [ + { + "path": "scene.fscene", + "sha256": "a7ac2dcd93dc9dce2d8ce860aa5dd838af2604dd7b40d5b50581d1a72bd5bf23", + "size": 9218 + }, + { + "path": "faset_player", + "sha256": "a68c39af919d5c04db4985f36e3da4573f9a4c13862bc599cd85724ac6da22dc", + "size": 5814720 + }, + { + "path": "shaders/vertexMain.spv", + "sha256": "1ad2631c35d654f48166321ae43d4165043e1b919b14f1dc61caf97b1ada0898", + "size": 1224 + }, + { + "path": "shaders/fragmentMain.spv", + "sha256": "f5c4f289917ecab5d00d543c053aac1d10e6cca311fd172daf06d315160a3505", + "size": 7436 + }, + { + "path": "shaders/shadowMain.spv", + "sha256": "7643b4d688492b5ee923b9606f0b0e70343ca05fa67673838206119a6f86d8d8", + "size": 784 + }, + { + "path": "shaders/vertexMain.reflection.json", + "sha256": "c584f3b957ebf194a292451362a98590f85ff3cfbfd8f917b1c1265039381777", + "size": 2639 + }, + { + "path": "shaders/fragmentMain.reflection.json", + "sha256": "72ebf8d32452830650416dc4717d89a4324e97ece7028dfb97c66803c8dce71f", + "size": 2649 + }, + { + "path": "shaders/shadowMain.reflection.json", + "sha256": "e3fd0889cd2f569d410f5d7558a4091d9f57f247a1dcf1788462f67525340819", + "size": 2645 + }, + { + "path": "assets/5832763b-3ed0-44d6-9088-0b524f196a91/generations/4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2/meshes/mesh-fc069a07a87e0f831fd92efdbc4efa1f-0.fmesh", + "sha256": "98b38aca7d2786c58a2265ba4d1f2255123f03d31d947e239232cf55bfdc2c97", + "size": 928 + }, + { + "path": "assets/5832763b-3ed0-44d6-9088-0b524f196a91/generations/4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2/meshes/mesh-0961e2a3f92977836a3aa80cab09277e-0.fmesh", + "sha256": "98b38aca7d2786c58a2265ba4d1f2255123f03d31d947e239232cf55bfdc2c97", + "size": 928 + }, + { + "path": "assets/5832763b-3ed0-44d6-9088-0b524f196a91/generations/4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2/meshes/mesh-93b5b3e52cb6bb120fefb80e2d8e222b-0.fmesh", + "sha256": "3875c16ea628d4126373ebfe07816621fbad3961596a2fcd112f1eec0e378071", + "size": 928 + }, + { + "path": "assets/5832763b-3ed0-44d6-9088-0b524f196a91/generations/4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2/manifest.json", + "sha256": "ee3f68794c6dc44ab492b1ba4640aa74e4de79a5ec10aed8dcdd45a03afc508e", + "size": 4532 + }, + { + "path": "assets/5832763b-3ed0-44d6-9088-0b524f196a91/current.json", + "sha256": "b0732959808ce61ef5a3223fba2d9ad8c17a3ea7873d4a81c00aadee6c51032e", + "size": 134 + }, + { + "path": "Notices/sdl3/LICENSE.txt", + "sha256": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127", + "size": 884 + }, + { + "path": "Notices/entt/LICENSE", + "sha256": "0785027ce472d7c61f05fba664a1d3ba6639c1593ced351ad9e4bed868765d99", + "size": 1097 + }, + { + "path": "Notices/box2d/LICENSE", + "sha256": "68a3e676d7e94093b102d5cba0d4e04af812040d6f230c3db67a6664574e43d2", + "size": 1067 + }, + { + "path": "Notices/box3d/LICENSE", + "sha256": "da5e31a26bf3cfd5ba5c96d6823e480128c81e76c107ef9d3ee5d94789184b90", + "size": 1067 + }, + { + "path": "Notices/json/LICENSE.MIT", + "sha256": "46a65cffd1ea955132d95a8dd921640714a8d6b537d2e4e482d31145ae95b603", + "size": 1076 + }, + { + "path": "Notices/stb/LICENSE", + "sha256": "bebfe904b14301657e4e5d655c811d51fd31b97c455b9cc2d8600d6bac6cff63", + "size": 2510 + }, + { + "path": "Notices/dependencies.json", + "sha256": "a54639ad2d9ad13c2e5f1ec90d6775f5dfc493656789b78c7e44debe2a378fdc", + "size": 2206 + }, + { + "path": "Notices/Faset-NOTICE.txt", + "sha256": "30c2e7e80f5153b31ab57675e07246aef9165965d64e36022b426a26449ece3f", + "size": 204 + }, + { + "path": "README.txt", + "sha256": "4dc2f0271c69fe39017ff96aa2dfa00b625bec1401d924dc396193ce71c09f3e", + "size": 225 + } + ], + "format": "faset.export", + "generation": "17281e17-9f4f-44f0-8821-6e0071e9a45b", + "platform": "linux", + "prerequisites": [ + "Linux x86_64", + "Vulkan 1.3 driver", + "Compatible glibc and libstdc++ runtime" + ], + "renderer_profile": { + "api": "Vulkan 1.3", + "materials": [ + "base-color factor and texture", + "metallic and roughness factors" + ], + "required_features": [ + "dynamicRendering", + "synchronization2" + ], + "shadow_map": { + "resolution": 1024, + "world_extent": 40 + }, + "texture_sampling": "linear clamp, one mip level", + "unsupported_material_features": [ + "normal maps", + "metallic-roughness maps", + "emissive and occlusion maps", + "alpha mode selection", + "unlit mode", + "per-material face culling" + ] + }, + "scene_hash": "6510f994ee01af5823614911fcdeff0fd433846d937ff1a65d5bf5fb08e45e8f", + "simulation": { + "fixed_delta": 0.016666666666666666, + "gravity": [ + 0, + -9.81, + 0 + ], + "max_catch_up_ticks": 4, + "physics_substeps": 4 + }, + "units": { + "angle": "radian", + "coordinates": "right-handed Y-up", + "distance": "metre" + }, + "validation_log": "warning: material material-bb63b094febbf90bb7049c9bcf7587bc has features beyond the initial base-color/PBR renderer\n{\"dimension\":3,\"validated\":true}\n", + "version": 1 +} diff --git a/docs/validation/linux-release-2026-09-18/collect-3d-profile-240.json b/docs/validation/linux-release-2026-09-18/collect-3d-profile-240.json new file mode 100644 index 0000000..3b449a9 --- /dev/null +++ b/docs/validation/linux-release-2026-09-18/collect-3d-profile-240.json @@ -0,0 +1,4637 @@ +{ + "completed_frames": 240, + "device": "NVIDIA GeForce RTX 2080 Ti", + "dimension": 3, + "fixed_delta_seconds": 0.016666666666666666, + "format": "faset.player-profile", + "height": 720, + "percentile_method": "nearest_rank_all_completed_frames_no_warmup_exclusion", + "presentation_mode": "offscreen", + "requested_frames": 240, + "resource_notes": "gpu_allocated_bytes sums live Vulkan memory allocations, including alignment, excluding driver internals. texture_count includes the white fallback texture.", + "samples": [ + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 1, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.596096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 4.807059, + "renderer_cpu_ms": 4.803463, + "renderer_readback_cpu_ms": 2.729419, + "simulation_ms": 0.243659, + "snapshot_ms": 0.740007, + "texture_count": 1, + "tick": 1, + "vertices": 576, + "wall_ms": 5.902556 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 2, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.592064, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.959588, + "renderer_cpu_ms": 1.95599, + "renderer_readback_cpu_ms": 0.473414, + "simulation_ms": 0.224503, + "snapshot_ms": 0.16393, + "texture_count": 1, + "tick": 2, + "vertices": 576, + "wall_ms": 2.357007 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 3, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.836464, + "renderer_cpu_ms": 1.833259, + "renderer_readback_cpu_ms": 0.444739, + "simulation_ms": 0.214164, + "snapshot_ms": 0.165522, + "texture_count": 1, + "tick": 3, + "vertices": 576, + "wall_ms": 2.226049 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 4, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588192, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.71216, + "renderer_cpu_ms": 1.709506, + "renderer_readback_cpu_ms": 0.459346, + "simulation_ms": 0.212561, + "snapshot_ms": 0.167706, + "texture_count": 1, + "tick": 4, + "vertices": 576, + "wall_ms": 2.102226 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 5, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.594816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.827858, + "renderer_cpu_ms": 1.824131, + "renderer_readback_cpu_ms": 0.444709, + "simulation_ms": 0.20708, + "snapshot_ms": 0.18555, + "texture_count": 1, + "tick": 5, + "vertices": 576, + "wall_ms": 2.229877 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 6, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587008, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.803953, + "renderer_cpu_ms": 1.800597, + "renderer_readback_cpu_ms": 0.43984, + "simulation_ms": 0.228361, + "snapshot_ms": 0.188586, + "texture_count": 1, + "tick": 6, + "vertices": 576, + "wall_ms": 2.230308 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 7, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.776923, + "renderer_cpu_ms": 1.773035, + "renderer_readback_cpu_ms": 0.500083, + "simulation_ms": 0.207992, + "snapshot_ms": 0.165573, + "texture_count": 1, + "tick": 7, + "vertices": 576, + "wall_ms": 2.160076 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 8, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587392, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.923249, + "renderer_cpu_ms": 1.919992, + "renderer_readback_cpu_ms": 0.45542, + "simulation_ms": 0.21701, + "snapshot_ms": 0.170171, + "texture_count": 1, + "tick": 8, + "vertices": 576, + "wall_ms": 2.320017 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 9, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587808, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.841424, + "renderer_cpu_ms": 1.83883, + "renderer_readback_cpu_ms": 0.463194, + "simulation_ms": 0.239151, + "snapshot_ms": 0.170542, + "texture_count": 1, + "tick": 9, + "vertices": 576, + "wall_ms": 2.261557 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 10, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.701139, + "renderer_cpu_ms": 1.697894, + "renderer_readback_cpu_ms": 0.486277, + "simulation_ms": 0.204035, + "snapshot_ms": 0.187504, + "texture_count": 1, + "tick": 10, + "vertices": 576, + "wall_ms": 2.101795 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 11, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.870329, + "renderer_cpu_ms": 1.867062, + "renderer_readback_cpu_ms": 0.439359, + "simulation_ms": 0.210547, + "snapshot_ms": 0.165883, + "texture_count": 1, + "tick": 11, + "vertices": 576, + "wall_ms": 2.256537 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 12, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.835824, + "renderer_cpu_ms": 1.833349, + "renderer_readback_cpu_ms": 0.437706, + "simulation_ms": 0.209806, + "snapshot_ms": 0.173507, + "texture_count": 1, + "tick": 12, + "vertices": 576, + "wall_ms": 2.228885 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 13, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.692474, + "renderer_cpu_ms": 1.689738, + "renderer_readback_cpu_ms": 0.442014, + "simulation_ms": 0.234612, + "snapshot_ms": 0.169891, + "texture_count": 1, + "tick": 13, + "vertices": 576, + "wall_ms": 2.105553 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 14, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.849259, + "renderer_cpu_ms": 1.846123, + "renderer_readback_cpu_ms": 0.440832, + "simulation_ms": 0.230184, + "snapshot_ms": 0.168468, + "texture_count": 1, + "tick": 14, + "vertices": 576, + "wall_ms": 2.258181 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 15, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.009892, + "renderer_cpu_ms": 2.006816, + "renderer_readback_cpu_ms": 0.440862, + "simulation_ms": 0.214234, + "snapshot_ms": 0.166504, + "texture_count": 1, + "tick": 15, + "vertices": 576, + "wall_ms": 2.400078 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 16, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.849479, + "renderer_cpu_ms": 1.845442, + "renderer_readback_cpu_ms": 0.497649, + "simulation_ms": 0.207692, + "snapshot_ms": 0.186422, + "texture_count": 1, + "tick": 16, + "vertices": 576, + "wall_ms": 2.252991 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 17, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.129277, + "renderer_cpu_ms": 2.126061, + "renderer_readback_cpu_ms": 0.464576, + "simulation_ms": 0.245553, + "snapshot_ms": 0.175822, + "texture_count": 1, + "tick": 17, + "vertices": 576, + "wall_ms": 2.561092 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 18, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.853437, + "renderer_cpu_ms": 1.850001, + "renderer_readback_cpu_ms": 0.417578, + "simulation_ms": 0.221668, + "snapshot_ms": 0.175742, + "texture_count": 1, + "tick": 18, + "vertices": 576, + "wall_ms": 2.261236 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 19, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.5872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.647448, + "renderer_cpu_ms": 1.644152, + "renderer_readback_cpu_ms": 0.437075, + "simulation_ms": 0.238691, + "snapshot_ms": 0.166053, + "texture_count": 1, + "tick": 19, + "vertices": 576, + "wall_ms": 2.060868 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 20, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586176, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.819483, + "renderer_cpu_ms": 1.815265, + "renderer_readback_cpu_ms": 0.459176, + "simulation_ms": 0.225365, + "snapshot_ms": 0.168759, + "texture_count": 1, + "tick": 20, + "vertices": 576, + "wall_ms": 2.222664 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 21, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59824, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.940932, + "renderer_cpu_ms": 1.937926, + "renderer_readback_cpu_ms": 0.413039, + "simulation_ms": 0.206219, + "snapshot_ms": 0.167476, + "texture_count": 1, + "tick": 21, + "vertices": 576, + "wall_ms": 2.323775 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 22, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.585568, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.734092, + "renderer_cpu_ms": 1.730465, + "renderer_readback_cpu_ms": 0.458936, + "simulation_ms": 0.22824, + "snapshot_ms": 0.16968, + "texture_count": 1, + "tick": 22, + "vertices": 576, + "wall_ms": 2.142332 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 23, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586944, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.79164, + "renderer_cpu_ms": 1.788294, + "renderer_readback_cpu_ms": 0.434069, + "simulation_ms": 0.212, + "snapshot_ms": 0.195398, + "texture_count": 1, + "tick": 23, + "vertices": 576, + "wall_ms": 2.209168 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 24, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586752, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.722479, + "renderer_cpu_ms": 1.718582, + "renderer_readback_cpu_ms": 0.456562, + "simulation_ms": 0.247607, + "snapshot_ms": 0.166244, + "texture_count": 1, + "tick": 24, + "vertices": 576, + "wall_ms": 2.14666 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 25, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587296, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.862113, + "renderer_cpu_ms": 1.858767, + "renderer_readback_cpu_ms": 0.464827, + "simulation_ms": 0.209535, + "snapshot_ms": 0.170512, + "texture_count": 1, + "tick": 25, + "vertices": 576, + "wall_ms": 2.251167 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 26, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587776, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.803763, + "renderer_cpu_ms": 1.800758, + "renderer_readback_cpu_ms": 0.417768, + "simulation_ms": 0.212852, + "snapshot_ms": 0.171834, + "texture_count": 1, + "tick": 26, + "vertices": 576, + "wall_ms": 2.197707 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 27, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587168, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.815815, + "renderer_cpu_ms": 1.812179, + "renderer_readback_cpu_ms": 0.533426, + "simulation_ms": 0.209656, + "snapshot_ms": 0.20138, + "texture_count": 1, + "tick": 27, + "vertices": 576, + "wall_ms": 2.235668 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 28, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.079583, + "renderer_cpu_ms": 2.075776, + "renderer_readback_cpu_ms": 0.488872, + "simulation_ms": 0.341915, + "snapshot_ms": 0.262044, + "texture_count": 1, + "tick": 28, + "vertices": 576, + "wall_ms": 2.694132 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 29, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.044177, + "renderer_cpu_ms": 2.04059, + "renderer_readback_cpu_ms": 0.493611, + "simulation_ms": 0.347084, + "snapshot_ms": 0.256795, + "texture_count": 1, + "tick": 29, + "vertices": 576, + "wall_ms": 2.661131 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 30, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587648, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.048996, + "renderer_cpu_ms": 2.044828, + "renderer_readback_cpu_ms": 0.465989, + "simulation_ms": 0.322028, + "snapshot_ms": 0.25461, + "texture_count": 1, + "tick": 30, + "vertices": 576, + "wall_ms": 2.635863 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 31, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587232, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.907729, + "renderer_cpu_ms": 1.902479, + "renderer_readback_cpu_ms": 0.472601, + "simulation_ms": 0.290718, + "snapshot_ms": 0.24908, + "texture_count": 1, + "tick": 31, + "vertices": 576, + "wall_ms": 2.456825 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 32, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.065157, + "renderer_cpu_ms": 2.060347, + "renderer_readback_cpu_ms": 0.526022, + "simulation_ms": 0.302431, + "snapshot_ms": 0.27604, + "texture_count": 1, + "tick": 32, + "vertices": 576, + "wall_ms": 2.654378 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 33, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.023959, + "renderer_cpu_ms": 2.01937, + "renderer_readback_cpu_ms": 0.479584, + "simulation_ms": 0.290488, + "snapshot_ms": 0.252867, + "texture_count": 1, + "tick": 33, + "vertices": 576, + "wall_ms": 2.579547 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 34, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587456, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.017036, + "renderer_cpu_ms": 2.013308, + "renderer_readback_cpu_ms": 0.460509, + "simulation_ms": 0.316217, + "snapshot_ms": 0.257976, + "texture_count": 1, + "tick": 34, + "vertices": 576, + "wall_ms": 2.602049 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 35, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.6008, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.252961, + "renderer_cpu_ms": 2.249193, + "renderer_readback_cpu_ms": 0.488601, + "simulation_ms": 0.294877, + "snapshot_ms": 0.271672, + "texture_count": 1, + "tick": 35, + "vertices": 576, + "wall_ms": 2.829669 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 36, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587776, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.954818, + "renderer_cpu_ms": 1.95028, + "renderer_readback_cpu_ms": 0.541342, + "simulation_ms": 0.287472, + "snapshot_ms": 0.250683, + "texture_count": 1, + "tick": 36, + "vertices": 576, + "wall_ms": 2.502722 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 37, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.593952, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.088961, + "renderer_cpu_ms": 2.084623, + "renderer_readback_cpu_ms": 0.525071, + "simulation_ms": 0.292492, + "snapshot_ms": 0.261844, + "texture_count": 1, + "tick": 37, + "vertices": 576, + "wall_ms": 2.669747 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 38, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.124088, + "renderer_cpu_ms": 2.119539, + "renderer_readback_cpu_ms": 0.533867, + "simulation_ms": 0.327047, + "snapshot_ms": 0.254681, + "texture_count": 1, + "tick": 38, + "vertices": 576, + "wall_ms": 2.716094 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 39, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.5864, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.477975, + "renderer_cpu_ms": 2.471764, + "renderer_readback_cpu_ms": 0.557151, + "simulation_ms": 0.326766, + "snapshot_ms": 0.284457, + "texture_count": 1, + "tick": 39, + "vertices": 576, + "wall_ms": 3.101442 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 40, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.584352, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.21038, + "renderer_cpu_ms": 2.205652, + "renderer_readback_cpu_ms": 0.531763, + "simulation_ms": 0.265431, + "snapshot_ms": 0.24385, + "texture_count": 1, + "tick": 40, + "vertices": 576, + "wall_ms": 2.732225 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 41, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.832949, + "renderer_cpu_ms": 1.82844, + "renderer_readback_cpu_ms": 0.539598, + "simulation_ms": 0.232228, + "snapshot_ms": 0.207021, + "texture_count": 1, + "tick": 41, + "vertices": 576, + "wall_ms": 2.282397 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 42, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.877212, + "renderer_cpu_ms": 1.873474, + "renderer_readback_cpu_ms": 0.493111, + "simulation_ms": 0.203645, + "snapshot_ms": 0.198734, + "texture_count": 1, + "tick": 42, + "vertices": 576, + "wall_ms": 2.292706 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 43, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.085054, + "renderer_cpu_ms": 2.079493, + "renderer_readback_cpu_ms": 0.675384, + "simulation_ms": 0.191811, + "snapshot_ms": 0.165302, + "texture_count": 1, + "tick": 43, + "vertices": 576, + "wall_ms": 2.453379 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 44, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.983682, + "renderer_cpu_ms": 1.980346, + "renderer_readback_cpu_ms": 0.494443, + "simulation_ms": 0.441703, + "snapshot_ms": 0.306739, + "texture_count": 1, + "tick": 44, + "vertices": 576, + "wall_ms": 2.744017 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 45, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59328, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.946433, + "renderer_cpu_ms": 1.942064, + "renderer_readback_cpu_ms": 0.480166, + "simulation_ms": 0.194326, + "snapshot_ms": 0.168318, + "texture_count": 1, + "tick": 45, + "vertices": 576, + "wall_ms": 2.318204 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 46, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587584, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.859268, + "renderer_cpu_ms": 1.855741, + "renderer_readback_cpu_ms": 0.458164, + "simulation_ms": 0.197253, + "snapshot_ms": 0.195488, + "texture_count": 1, + "tick": 46, + "vertices": 576, + "wall_ms": 2.262098 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 47, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.710567, + "renderer_cpu_ms": 1.707412, + "renderer_readback_cpu_ms": 0.456371, + "simulation_ms": 0.207201, + "snapshot_ms": 0.171534, + "texture_count": 1, + "tick": 47, + "vertices": 576, + "wall_ms": 2.09905 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 48, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.596224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.837427, + "renderer_cpu_ms": 1.83419, + "renderer_readback_cpu_ms": 0.457994, + "simulation_ms": 0.193275, + "snapshot_ms": 0.170702, + "texture_count": 1, + "tick": 48, + "vertices": 576, + "wall_ms": 2.210861 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 49, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.599008, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.021975, + "renderer_cpu_ms": 2.017566, + "renderer_readback_cpu_ms": 0.556691, + "simulation_ms": 0.189397, + "snapshot_ms": 0.163669, + "texture_count": 1, + "tick": 49, + "vertices": 576, + "wall_ms": 2.3852 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 50, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.823581, + "renderer_cpu_ms": 1.819072, + "renderer_readback_cpu_ms": 0.502658, + "simulation_ms": 0.294255, + "snapshot_ms": 0.251375, + "texture_count": 1, + "tick": 50, + "vertices": 576, + "wall_ms": 2.382194 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 51, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587744, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.916806, + "renderer_cpu_ms": 1.91344, + "renderer_readback_cpu_ms": 0.468995, + "simulation_ms": 0.202032, + "snapshot_ms": 0.173066, + "texture_count": 1, + "tick": 51, + "vertices": 576, + "wall_ms": 2.301963 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 52, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587808, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.912668, + "renderer_cpu_ms": 1.907569, + "renderer_readback_cpu_ms": 0.435382, + "simulation_ms": 0.199696, + "snapshot_ms": 0.188626, + "texture_count": 1, + "tick": 52, + "vertices": 576, + "wall_ms": 2.312833 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 53, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587424, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.171127, + "renderer_cpu_ms": 2.165866, + "renderer_readback_cpu_ms": 0.920166, + "simulation_ms": 0.211679, + "snapshot_ms": 0.168759, + "texture_count": 1, + "tick": 53, + "vertices": 576, + "wall_ms": 2.560662 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 54, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587648, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.863115, + "renderer_cpu_ms": 1.859889, + "renderer_readback_cpu_ms": 0.458194, + "simulation_ms": 0.190059, + "snapshot_ms": 0.164811, + "texture_count": 1, + "tick": 54, + "vertices": 576, + "wall_ms": 2.227843 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 55, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587168, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.8818, + "renderer_cpu_ms": 1.878044, + "renderer_readback_cpu_ms": 0.459256, + "simulation_ms": 0.205528, + "snapshot_ms": 0.16416, + "texture_count": 1, + "tick": 55, + "vertices": 576, + "wall_ms": 2.262518 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 56, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586944, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.776502, + "renderer_cpu_ms": 1.772764, + "renderer_readback_cpu_ms": 0.492058, + "simulation_ms": 0.212672, + "snapshot_ms": 0.16987, + "texture_count": 1, + "tick": 56, + "vertices": 576, + "wall_ms": 2.168572 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 57, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.163883, + "renderer_cpu_ms": 2.160066, + "renderer_readback_cpu_ms": 0.724697, + "simulation_ms": 0.189157, + "snapshot_ms": 0.168307, + "texture_count": 1, + "tick": 57, + "vertices": 576, + "wall_ms": 2.530705 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 58, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588128, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.917086, + "renderer_cpu_ms": 1.9136, + "renderer_readback_cpu_ms": 0.454768, + "simulation_ms": 0.214184, + "snapshot_ms": 0.17464, + "texture_count": 1, + "tick": 58, + "vertices": 576, + "wall_ms": 2.315288 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 59, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587424, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.765291, + "renderer_cpu_ms": 1.761373, + "renderer_readback_cpu_ms": 0.454878, + "simulation_ms": 0.199256, + "snapshot_ms": 0.175632, + "texture_count": 1, + "tick": 59, + "vertices": 576, + "wall_ms": 2.150167 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 60, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.894014, + "renderer_cpu_ms": 1.890587, + "renderer_readback_cpu_ms": 0.46639, + "simulation_ms": 0.199066, + "snapshot_ms": 0.175561, + "texture_count": 1, + "tick": 60, + "vertices": 576, + "wall_ms": 2.27862 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 61, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.868946, + "renderer_cpu_ms": 1.865379, + "renderer_readback_cpu_ms": 0.46622, + "simulation_ms": 0.246645, + "snapshot_ms": 0.174089, + "texture_count": 1, + "tick": 61, + "vertices": 576, + "wall_ms": 2.298847 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 62, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.774178, + "renderer_cpu_ms": 1.768667, + "renderer_readback_cpu_ms": 0.501376, + "simulation_ms": 0.216208, + "snapshot_ms": 0.171534, + "texture_count": 1, + "tick": 62, + "vertices": 576, + "wall_ms": 2.175625 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 63, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.000033, + "renderer_cpu_ms": 1.996898, + "renderer_readback_cpu_ms": 0.514561, + "simulation_ms": 0.192413, + "snapshot_ms": 0.16964, + "texture_count": 1, + "tick": 63, + "vertices": 576, + "wall_ms": 2.371815 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 64, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588384, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.963364, + "renderer_cpu_ms": 1.959547, + "renderer_readback_cpu_ms": 0.547082, + "simulation_ms": 0.200939, + "snapshot_ms": 0.222419, + "texture_count": 1, + "tick": 64, + "vertices": 576, + "wall_ms": 2.39616 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 65, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59728, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.789637, + "renderer_cpu_ms": 1.786781, + "renderer_readback_cpu_ms": 0.480627, + "simulation_ms": 0.204115, + "snapshot_ms": 0.247436, + "texture_count": 1, + "tick": 65, + "vertices": 576, + "wall_ms": 2.250366 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 66, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588288, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.376093, + "renderer_cpu_ms": 2.371885, + "renderer_readback_cpu_ms": 0.920336, + "simulation_ms": 0.206309, + "snapshot_ms": 0.171964, + "texture_count": 1, + "tick": 66, + "vertices": 576, + "wall_ms": 2.763584 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 67, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.965308, + "renderer_cpu_ms": 1.962172, + "renderer_readback_cpu_ms": 0.507358, + "simulation_ms": 0.193565, + "snapshot_ms": 0.169961, + "texture_count": 1, + "tick": 67, + "vertices": 576, + "wall_ms": 2.675598 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 68, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588352, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.049777, + "renderer_cpu_ms": 2.046471, + "renderer_readback_cpu_ms": 0.521143, + "simulation_ms": 0.213223, + "snapshot_ms": 0.206199, + "texture_count": 1, + "tick": 68, + "vertices": 576, + "wall_ms": 2.479407 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 69, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.168151, + "renderer_cpu_ms": 2.140839, + "renderer_readback_cpu_ms": 0.483823, + "simulation_ms": 0.194757, + "snapshot_ms": 0.174199, + "texture_count": 1, + "tick": 69, + "vertices": 576, + "wall_ms": 2.548608 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 70, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.956802, + "renderer_cpu_ms": 1.953466, + "renderer_readback_cpu_ms": 0.721932, + "simulation_ms": 0.192563, + "snapshot_ms": 0.170031, + "texture_count": 1, + "tick": 70, + "vertices": 576, + "wall_ms": 2.330296 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 71, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.899143, + "renderer_cpu_ms": 1.895306, + "renderer_readback_cpu_ms": 0.484374, + "simulation_ms": 0.196581, + "snapshot_ms": 0.165612, + "texture_count": 1, + "tick": 71, + "vertices": 576, + "wall_ms": 2.272598 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 72, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588256, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.851503, + "renderer_cpu_ms": 1.848388, + "renderer_readback_cpu_ms": 0.428548, + "simulation_ms": 0.193976, + "snapshot_ms": 0.200969, + "texture_count": 1, + "tick": 72, + "vertices": 576, + "wall_ms": 2.256227 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 73, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.065688, + "renderer_cpu_ms": 2.061139, + "renderer_readback_cpu_ms": 0.780502, + "simulation_ms": 0.215286, + "snapshot_ms": 0.16965, + "texture_count": 1, + "tick": 73, + "vertices": 576, + "wall_ms": 2.460743 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 74, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.5936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.013088, + "renderer_cpu_ms": 2.009301, + "renderer_readback_cpu_ms": 0.49283, + "simulation_ms": 0.275219, + "snapshot_ms": 0.195439, + "texture_count": 1, + "tick": 74, + "vertices": 576, + "wall_ms": 2.496841 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 75, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.821957, + "renderer_cpu_ms": 1.818571, + "renderer_readback_cpu_ms": 0.432977, + "simulation_ms": 0.192463, + "snapshot_ms": 0.167596, + "texture_count": 1, + "tick": 75, + "vertices": 576, + "wall_ms": 2.191394 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 76, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.748559, + "renderer_cpu_ms": 1.744932, + "renderer_readback_cpu_ms": 0.464878, + "simulation_ms": 0.204035, + "snapshot_ms": 0.162216, + "texture_count": 1, + "tick": 76, + "vertices": 576, + "wall_ms": 2.127834 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 77, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.86611, + "renderer_cpu_ms": 1.861993, + "renderer_readback_cpu_ms": 0.447875, + "simulation_ms": 0.200068, + "snapshot_ms": 0.170331, + "texture_count": 1, + "tick": 77, + "vertices": 576, + "wall_ms": 2.246398 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 78, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.788264, + "renderer_cpu_ms": 1.784858, + "renderer_readback_cpu_ms": 0.407519, + "simulation_ms": 0.196831, + "snapshot_ms": 0.190339, + "texture_count": 1, + "tick": 78, + "vertices": 576, + "wall_ms": 2.184873 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 79, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587648, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.644403, + "renderer_cpu_ms": 1.626759, + "renderer_readback_cpu_ms": 0.436092, + "simulation_ms": 0.203634, + "snapshot_ms": 0.183166, + "texture_count": 1, + "tick": 79, + "vertices": 576, + "wall_ms": 2.040269 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 80, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587648, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.801589, + "renderer_cpu_ms": 1.797401, + "renderer_readback_cpu_ms": 0.446813, + "simulation_ms": 0.201019, + "snapshot_ms": 0.180551, + "texture_count": 1, + "tick": 80, + "vertices": 576, + "wall_ms": 2.193218 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 81, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.868184, + "renderer_cpu_ms": 1.863706, + "renderer_readback_cpu_ms": 0.456662, + "simulation_ms": 0.197934, + "snapshot_ms": 0.167596, + "texture_count": 1, + "tick": 81, + "vertices": 576, + "wall_ms": 2.244244 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 82, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.816406, + "renderer_cpu_ms": 1.812439, + "renderer_readback_cpu_ms": 0.51905, + "simulation_ms": 0.254681, + "snapshot_ms": 0.237578, + "texture_count": 1, + "tick": 82, + "vertices": 576, + "wall_ms": 2.320047 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 83, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.801478, + "renderer_cpu_ms": 1.798213, + "renderer_readback_cpu_ms": 0.42932, + "simulation_ms": 0.2065, + "snapshot_ms": 0.168668, + "texture_count": 1, + "tick": 83, + "vertices": 576, + "wall_ms": 2.186785 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 84, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.834081, + "renderer_cpu_ms": 1.831305, + "renderer_readback_cpu_ms": 0.487209, + "simulation_ms": 0.21196, + "snapshot_ms": 0.165923, + "texture_count": 1, + "tick": 84, + "vertices": 576, + "wall_ms": 2.221291 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 85, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.72287, + "renderer_cpu_ms": 1.720175, + "renderer_readback_cpu_ms": 0.433728, + "simulation_ms": 0.194236, + "snapshot_ms": 0.162066, + "texture_count": 1, + "tick": 85, + "vertices": 576, + "wall_ms": 2.088751 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 86, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588064, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.848778, + "renderer_cpu_ms": 1.845783, + "renderer_readback_cpu_ms": 0.409883, + "simulation_ms": 0.188786, + "snapshot_ms": 0.16435, + "texture_count": 1, + "tick": 86, + "vertices": 576, + "wall_ms": 2.210961 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 87, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588224, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.726117, + "renderer_cpu_ms": 1.723021, + "renderer_readback_cpu_ms": 0.422236, + "simulation_ms": 0.195108, + "snapshot_ms": 0.192633, + "texture_count": 1, + "tick": 87, + "vertices": 576, + "wall_ms": 2.123767 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 88, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586272, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.894535, + "renderer_cpu_ms": 1.890957, + "renderer_readback_cpu_ms": 0.463725, + "simulation_ms": 0.232018, + "snapshot_ms": 0.19632, + "texture_count": 1, + "tick": 88, + "vertices": 576, + "wall_ms": 2.332802 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 89, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59552, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.977561, + "renderer_cpu_ms": 1.972892, + "renderer_readback_cpu_ms": 0.519069, + "simulation_ms": 0.196461, + "snapshot_ms": 0.167936, + "texture_count": 1, + "tick": 89, + "vertices": 576, + "wall_ms": 2.351487 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 90, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586144, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.059065, + "renderer_cpu_ms": 2.054797, + "renderer_readback_cpu_ms": 0.4773, + "simulation_ms": 0.330243, + "snapshot_ms": 0.233361, + "texture_count": 1, + "tick": 90, + "vertices": 576, + "wall_ms": 2.632436 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 91, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586944, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.744892, + "renderer_cpu_ms": 1.741877, + "renderer_readback_cpu_ms": 0.451752, + "simulation_ms": 0.201219, + "snapshot_ms": 0.163609, + "texture_count": 1, + "tick": 91, + "vertices": 576, + "wall_ms": 2.121513 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 92, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.846163, + "renderer_cpu_ms": 1.842045, + "renderer_readback_cpu_ms": 0.424751, + "simulation_ms": 0.197272, + "snapshot_ms": 0.172356, + "texture_count": 1, + "tick": 92, + "vertices": 576, + "wall_ms": 2.225719 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 93, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58752, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.970568, + "renderer_cpu_ms": 1.96622, + "renderer_readback_cpu_ms": 0.448246, + "simulation_ms": 0.283835, + "snapshot_ms": 0.250844, + "texture_count": 1, + "tick": 93, + "vertices": 576, + "wall_ms": 2.516648 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 94, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58688, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.753599, + "renderer_cpu_ms": 1.749912, + "renderer_readback_cpu_ms": 0.491327, + "simulation_ms": 0.188576, + "snapshot_ms": 0.165993, + "texture_count": 1, + "tick": 94, + "vertices": 576, + "wall_ms": 2.117656 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 95, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587904, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.808001, + "renderer_cpu_ms": 1.804044, + "renderer_readback_cpu_ms": 0.443346, + "simulation_ms": 0.194507, + "snapshot_ms": 0.16996, + "texture_count": 1, + "tick": 95, + "vertices": 576, + "wall_ms": 2.182578 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 96, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587232, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.943496, + "renderer_cpu_ms": 1.93974, + "renderer_readback_cpu_ms": 0.43485, + "simulation_ms": 0.235544, + "snapshot_ms": 0.25405, + "texture_count": 1, + "tick": 96, + "vertices": 576, + "wall_ms": 2.446786 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 97, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586368, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.737548, + "renderer_cpu_ms": 1.734503, + "renderer_readback_cpu_ms": 0.46139, + "simulation_ms": 0.19047, + "snapshot_ms": 0.171894, + "texture_count": 1, + "tick": 97, + "vertices": 576, + "wall_ms": 2.110071 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 98, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587296, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.791129, + "renderer_cpu_ms": 1.787803, + "renderer_readback_cpu_ms": 0.421155, + "simulation_ms": 0.193716, + "snapshot_ms": 0.171263, + "texture_count": 1, + "tick": 98, + "vertices": 576, + "wall_ms": 2.166708 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 99, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.70673, + "renderer_cpu_ms": 1.703564, + "renderer_readback_cpu_ms": 0.41837, + "simulation_ms": 0.193385, + "snapshot_ms": 0.165943, + "texture_count": 1, + "tick": 99, + "vertices": 576, + "wall_ms": 2.076227 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 100, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587584, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.861391, + "renderer_cpu_ms": 1.858106, + "renderer_readback_cpu_ms": 0.436042, + "simulation_ms": 0.210928, + "snapshot_ms": 0.181913, + "texture_count": 1, + "tick": 100, + "vertices": 576, + "wall_ms": 2.264271 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 101, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.624064, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.929691, + "renderer_cpu_ms": 1.926394, + "renderer_readback_cpu_ms": 0.456431, + "simulation_ms": 0.194558, + "snapshot_ms": 0.170541, + "texture_count": 1, + "tick": 101, + "vertices": 576, + "wall_ms": 2.324957 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 102, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586336, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.727379, + "renderer_cpu_ms": 1.724233, + "renderer_readback_cpu_ms": 0.430623, + "simulation_ms": 0.192433, + "snapshot_ms": 0.171494, + "texture_count": 1, + "tick": 102, + "vertices": 576, + "wall_ms": 2.101315 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 103, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58752, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.965328, + "renderer_cpu_ms": 1.961951, + "renderer_readback_cpu_ms": 0.508941, + "simulation_ms": 0.19076, + "snapshot_ms": 0.19087, + "texture_count": 1, + "tick": 103, + "vertices": 576, + "wall_ms": 2.356215 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 104, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58752, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.985897, + "renderer_cpu_ms": 1.981849, + "renderer_readback_cpu_ms": 0.431744, + "simulation_ms": 0.222039, + "snapshot_ms": 0.210777, + "texture_count": 1, + "tick": 104, + "vertices": 576, + "wall_ms": 2.429133 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 105, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586176, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.714455, + "renderer_cpu_ms": 1.711208, + "renderer_readback_cpu_ms": 0.443487, + "simulation_ms": 0.199226, + "snapshot_ms": 0.175271, + "texture_count": 1, + "tick": 105, + "vertices": 576, + "wall_ms": 2.099391 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 106, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586528, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.943187, + "renderer_cpu_ms": 1.939249, + "renderer_readback_cpu_ms": 0.444599, + "simulation_ms": 0.185701, + "snapshot_ms": 0.164961, + "texture_count": 1, + "tick": 106, + "vertices": 576, + "wall_ms": 2.302234 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 107, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.602592, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.996146, + "renderer_cpu_ms": 1.99278, + "renderer_readback_cpu_ms": 0.42412, + "simulation_ms": 0.207472, + "snapshot_ms": 0.170321, + "texture_count": 1, + "tick": 107, + "vertices": 576, + "wall_ms": 2.383798 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 108, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.585824, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.760973, + "renderer_cpu_ms": 1.757515, + "renderer_readback_cpu_ms": 0.485186, + "simulation_ms": 0.191872, + "snapshot_ms": 0.165853, + "texture_count": 1, + "tick": 108, + "vertices": 576, + "wall_ms": 2.128386 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 109, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.873324, + "renderer_cpu_ms": 1.869938, + "renderer_readback_cpu_ms": 0.449849, + "simulation_ms": 0.195348, + "snapshot_ms": 0.1696, + "texture_count": 1, + "tick": 109, + "vertices": 576, + "wall_ms": 2.247601 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 110, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.124468, + "renderer_cpu_ms": 2.120461, + "renderer_readback_cpu_ms": 0.459718, + "simulation_ms": 0.192202, + "snapshot_ms": 0.190329, + "texture_count": 1, + "tick": 110, + "vertices": 576, + "wall_ms": 2.515937 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 111, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586464, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.696941, + "renderer_cpu_ms": 1.692544, + "renderer_readback_cpu_ms": 0.445761, + "simulation_ms": 0.219765, + "snapshot_ms": 0.171243, + "texture_count": 1, + "tick": 111, + "vertices": 576, + "wall_ms": 2.097738 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 112, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588192, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.889956, + "renderer_cpu_ms": 1.885487, + "renderer_readback_cpu_ms": 0.452544, + "simulation_ms": 0.190991, + "snapshot_ms": 0.166273, + "texture_count": 1, + "tick": 112, + "vertices": 576, + "wall_ms": 2.25743 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 113, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.097157, + "renderer_cpu_ms": 2.094542, + "renderer_readback_cpu_ms": 0.449528, + "simulation_ms": 0.19659, + "snapshot_ms": 0.186212, + "texture_count": 1, + "tick": 113, + "vertices": 576, + "wall_ms": 2.488756 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 114, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.806939, + "renderer_cpu_ms": 1.804435, + "renderer_readback_cpu_ms": 0.474395, + "simulation_ms": 0.215006, + "snapshot_ms": 0.16977, + "texture_count": 1, + "tick": 114, + "vertices": 576, + "wall_ms": 2.202606 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 115, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587488, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.130359, + "renderer_cpu_ms": 2.125942, + "renderer_readback_cpu_ms": 0.636972, + "simulation_ms": 0.193875, + "snapshot_ms": 0.165322, + "texture_count": 1, + "tick": 115, + "vertices": 576, + "wall_ms": 2.499836 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 116, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.016555, + "renderer_cpu_ms": 2.013178, + "renderer_readback_cpu_ms": 0.463975, + "simulation_ms": 0.300988, + "snapshot_ms": 0.242177, + "texture_count": 1, + "tick": 116, + "vertices": 576, + "wall_ms": 2.580238 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 117, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587808, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.846234, + "renderer_cpu_ms": 1.843668, + "renderer_readback_cpu_ms": 0.458395, + "simulation_ms": 0.201791, + "snapshot_ms": 0.170802, + "texture_count": 1, + "tick": 117, + "vertices": 576, + "wall_ms": 2.228094 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 118, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586848, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.72282, + "renderer_cpu_ms": 1.720266, + "renderer_readback_cpu_ms": 0.418771, + "simulation_ms": 0.204546, + "snapshot_ms": 0.167506, + "texture_count": 1, + "tick": 118, + "vertices": 576, + "wall_ms": 2.103629 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 119, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.92409, + "renderer_cpu_ms": 1.920824, + "renderer_readback_cpu_ms": 0.469886, + "simulation_ms": 0.240153, + "snapshot_ms": 0.223972, + "texture_count": 1, + "tick": 119, + "vertices": 576, + "wall_ms": 2.396902 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 120, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.987309, + "renderer_cpu_ms": 1.983122, + "renderer_readback_cpu_ms": 0.437295, + "simulation_ms": 0.200478, + "snapshot_ms": 0.16977, + "texture_count": 1, + "tick": 120, + "vertices": 576, + "wall_ms": 2.367767 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 121, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587488, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.708654, + "renderer_cpu_ms": 1.705338, + "renderer_readback_cpu_ms": 0.443046, + "simulation_ms": 0.230585, + "snapshot_ms": 0.17514, + "texture_count": 1, + "tick": 121, + "vertices": 576, + "wall_ms": 2.12504 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 122, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586272, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.846253, + "renderer_cpu_ms": 1.842707, + "renderer_readback_cpu_ms": 0.466851, + "simulation_ms": 0.215116, + "snapshot_ms": 0.163889, + "texture_count": 1, + "tick": 122, + "vertices": 576, + "wall_ms": 2.235267 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 123, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.866432, + "renderer_cpu_ms": 1.862284, + "renderer_readback_cpu_ms": 0.562321, + "simulation_ms": 0.196681, + "snapshot_ms": 0.164791, + "texture_count": 1, + "tick": 123, + "vertices": 576, + "wall_ms": 2.237832 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 124, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586752, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.942215, + "renderer_cpu_ms": 1.938958, + "renderer_readback_cpu_ms": 0.461952, + "simulation_ms": 0.218682, + "snapshot_ms": 0.192604, + "texture_count": 1, + "tick": 124, + "vertices": 576, + "wall_ms": 2.365383 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 125, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.867864, + "renderer_cpu_ms": 1.864177, + "renderer_readback_cpu_ms": 0.488181, + "simulation_ms": 0.225806, + "snapshot_ms": 0.177395, + "texture_count": 1, + "tick": 125, + "vertices": 576, + "wall_ms": 2.281033 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 126, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.761643, + "renderer_cpu_ms": 1.759089, + "renderer_readback_cpu_ms": 0.457774, + "simulation_ms": 0.20643, + "snapshot_ms": 0.176112, + "texture_count": 1, + "tick": 126, + "vertices": 576, + "wall_ms": 2.156067 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 127, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.91855, + "renderer_cpu_ms": 1.915414, + "renderer_readback_cpu_ms": 0.479124, + "simulation_ms": 0.202813, + "snapshot_ms": 0.173237, + "texture_count": 1, + "tick": 127, + "vertices": 576, + "wall_ms": 2.304007 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 128, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587776, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.834401, + "renderer_cpu_ms": 1.830814, + "renderer_readback_cpu_ms": 0.43448, + "simulation_ms": 0.204947, + "snapshot_ms": 0.194898, + "texture_count": 1, + "tick": 128, + "vertices": 576, + "wall_ms": 2.245948 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 129, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587296, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.734753, + "renderer_cpu_ms": 1.729904, + "renderer_readback_cpu_ms": 0.455599, + "simulation_ms": 0.187564, + "snapshot_ms": 0.165412, + "texture_count": 1, + "tick": 129, + "vertices": 576, + "wall_ms": 2.097117 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 130, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587264, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.895927, + "renderer_cpu_ms": 1.893052, + "renderer_readback_cpu_ms": 0.458636, + "simulation_ms": 0.202732, + "snapshot_ms": 0.172135, + "texture_count": 1, + "tick": 130, + "vertices": 576, + "wall_ms": 2.280433 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 131, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.97714, + "renderer_cpu_ms": 1.974195, + "renderer_readback_cpu_ms": 0.398812, + "simulation_ms": 0.200899, + "snapshot_ms": 0.171995, + "texture_count": 1, + "tick": 131, + "vertices": 576, + "wall_ms": 2.359843 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 132, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586624, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.690891, + "renderer_cpu_ms": 1.687364, + "renderer_readback_cpu_ms": 0.44541, + "simulation_ms": 0.210297, + "snapshot_ms": 0.163819, + "texture_count": 1, + "tick": 132, + "vertices": 576, + "wall_ms": 2.074715 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 133, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587328, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.999092, + "renderer_cpu_ms": 1.995495, + "renderer_readback_cpu_ms": 0.469426, + "simulation_ms": 0.254881, + "snapshot_ms": 0.192353, + "texture_count": 1, + "tick": 133, + "vertices": 576, + "wall_ms": 2.457186 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 134, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.839962, + "renderer_cpu_ms": 1.835383, + "renderer_readback_cpu_ms": 0.446433, + "simulation_ms": 0.249761, + "snapshot_ms": 0.195209, + "texture_count": 1, + "tick": 134, + "vertices": 576, + "wall_ms": 2.297004 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 135, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586816, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.693114, + "renderer_cpu_ms": 1.689598, + "renderer_readback_cpu_ms": 0.435872, + "simulation_ms": 0.212782, + "snapshot_ms": 0.16983, + "texture_count": 1, + "tick": 135, + "vertices": 576, + "wall_ms": 2.086136 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 136, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587584, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.902199, + "renderer_cpu_ms": 1.898522, + "renderer_readback_cpu_ms": 0.474205, + "simulation_ms": 0.190179, + "snapshot_ms": 0.164751, + "texture_count": 1, + "tick": 136, + "vertices": 576, + "wall_ms": 2.277837 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 137, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.5872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.179593, + "renderer_cpu_ms": 2.174082, + "renderer_readback_cpu_ms": 0.429641, + "simulation_ms": 0.193064, + "snapshot_ms": 0.170131, + "texture_count": 1, + "tick": 137, + "vertices": 576, + "wall_ms": 2.552737 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 138, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587008, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.69094, + "renderer_cpu_ms": 1.687443, + "renderer_readback_cpu_ms": 0.442455, + "simulation_ms": 0.214665, + "snapshot_ms": 0.166824, + "texture_count": 1, + "tick": 138, + "vertices": 576, + "wall_ms": 2.083491 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 139, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586176, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.88144, + "renderer_cpu_ms": 1.877732, + "renderer_readback_cpu_ms": 0.480497, + "simulation_ms": 0.193906, + "snapshot_ms": 0.167055, + "texture_count": 1, + "tick": 139, + "vertices": 576, + "wall_ms": 2.253512 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 140, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587168, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.025231, + "renderer_cpu_ms": 2.021844, + "renderer_readback_cpu_ms": 0.428829, + "simulation_ms": 0.197682, + "snapshot_ms": 0.171193, + "texture_count": 1, + "tick": 140, + "vertices": 576, + "wall_ms": 2.402713 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 141, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.5856, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.663869, + "renderer_cpu_ms": 1.660884, + "renderer_readback_cpu_ms": 0.450059, + "simulation_ms": 0.197623, + "snapshot_ms": 0.182294, + "texture_count": 1, + "tick": 141, + "vertices": 576, + "wall_ms": 2.055468 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 142, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.884165, + "renderer_cpu_ms": 1.880197, + "renderer_readback_cpu_ms": 0.487048, + "simulation_ms": 0.205879, + "snapshot_ms": 0.16438, + "texture_count": 1, + "tick": 142, + "vertices": 576, + "wall_ms": 2.262879 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 143, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.60608, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.029388, + "renderer_cpu_ms": 2.025742, + "renderer_readback_cpu_ms": 0.436313, + "simulation_ms": 0.198445, + "snapshot_ms": 0.165071, + "texture_count": 1, + "tick": 143, + "vertices": 576, + "wall_ms": 2.402813 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 144, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586528, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.777684, + "renderer_cpu_ms": 1.774739, + "renderer_readback_cpu_ms": 0.444779, + "simulation_ms": 0.191592, + "snapshot_ms": 0.168808, + "texture_count": 1, + "tick": 144, + "vertices": 576, + "wall_ms": 2.14643 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 145, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587392, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.868365, + "renderer_cpu_ms": 1.864708, + "renderer_readback_cpu_ms": 0.442475, + "simulation_ms": 0.194667, + "snapshot_ms": 0.185219, + "texture_count": 1, + "tick": 145, + "vertices": 576, + "wall_ms": 2.25795 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 146, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.612, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.223645, + "renderer_cpu_ms": 2.220079, + "renderer_readback_cpu_ms": 0.534609, + "simulation_ms": 0.195589, + "snapshot_ms": 0.166965, + "texture_count": 1, + "tick": 146, + "vertices": 576, + "wall_ms": 2.596238 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 147, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.73303, + "renderer_cpu_ms": 1.730596, + "renderer_readback_cpu_ms": 0.447895, + "simulation_ms": 0.200518, + "snapshot_ms": 0.208363, + "texture_count": 1, + "tick": 147, + "vertices": 576, + "wall_ms": 2.153283 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 148, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587552, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.918149, + "renderer_cpu_ms": 1.914462, + "renderer_readback_cpu_ms": 0.499863, + "simulation_ms": 0.215537, + "snapshot_ms": 0.173558, + "texture_count": 1, + "tick": 148, + "vertices": 576, + "wall_ms": 2.317122 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 149, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.011775, + "renderer_cpu_ms": 2.007808, + "renderer_readback_cpu_ms": 0.466821, + "simulation_ms": 0.193455, + "snapshot_ms": 0.168027, + "texture_count": 1, + "tick": 149, + "vertices": 576, + "wall_ms": 2.382425 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 150, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587424, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 3.142218, + "renderer_cpu_ms": 3.13733, + "renderer_readback_cpu_ms": 0.608337, + "simulation_ms": 0.201791, + "snapshot_ms": 0.191852, + "texture_count": 1, + "tick": 150, + "vertices": 576, + "wall_ms": 3.54584 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 151, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588576, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.172248, + "renderer_cpu_ms": 2.168741, + "renderer_readback_cpu_ms": 0.577991, + "simulation_ms": 0.222309, + "snapshot_ms": 0.20152, + "texture_count": 1, + "tick": 151, + "vertices": 576, + "wall_ms": 2.60784 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 152, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.107797, + "renderer_cpu_ms": 2.10451, + "renderer_readback_cpu_ms": 0.487861, + "simulation_ms": 0.273597, + "snapshot_ms": 0.230545, + "texture_count": 1, + "tick": 152, + "vertices": 576, + "wall_ms": 2.62351 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 153, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587136, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.041241, + "renderer_cpu_ms": 2.036973, + "renderer_readback_cpu_ms": 0.511565, + "simulation_ms": 0.27031, + "snapshot_ms": 0.483973, + "texture_count": 1, + "tick": 153, + "vertices": 576, + "wall_ms": 2.805523 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 154, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587744, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.11472, + "renderer_cpu_ms": 2.111023, + "renderer_readback_cpu_ms": 0.553775, + "simulation_ms": 0.275079, + "snapshot_ms": 0.237568, + "texture_count": 1, + "tick": 154, + "vertices": 576, + "wall_ms": 2.638258 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 155, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587584, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.021794, + "renderer_cpu_ms": 2.018128, + "renderer_readback_cpu_ms": 0.489193, + "simulation_ms": 0.277113, + "snapshot_ms": 0.244671, + "texture_count": 1, + "tick": 155, + "vertices": 576, + "wall_ms": 2.554419 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 156, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.954738, + "renderer_cpu_ms": 1.95008, + "renderer_readback_cpu_ms": 0.591887, + "simulation_ms": 0.193345, + "snapshot_ms": 0.233531, + "texture_count": 1, + "tick": 156, + "vertices": 576, + "wall_ms": 2.390781 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 157, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58432, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.919452, + "renderer_cpu_ms": 1.916596, + "renderer_readback_cpu_ms": 0.473313, + "simulation_ms": 0.28095, + "snapshot_ms": 0.237549, + "texture_count": 1, + "tick": 157, + "vertices": 576, + "wall_ms": 2.447999 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 158, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.600704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.769398, + "renderer_cpu_ms": 1.766232, + "renderer_readback_cpu_ms": 0.412558, + "simulation_ms": 0.193736, + "snapshot_ms": 0.182584, + "texture_count": 1, + "tick": 158, + "vertices": 576, + "wall_ms": 2.155046 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 159, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.710317, + "renderer_cpu_ms": 1.706289, + "renderer_readback_cpu_ms": 0.504692, + "simulation_ms": 0.233179, + "snapshot_ms": 0.163008, + "texture_count": 1, + "tick": 159, + "vertices": 576, + "wall_ms": 2.115932 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 160, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.594144, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.884636, + "renderer_cpu_ms": 1.88169, + "renderer_readback_cpu_ms": 0.483572, + "simulation_ms": 0.242257, + "snapshot_ms": 0.205838, + "texture_count": 1, + "tick": 160, + "vertices": 576, + "wall_ms": 2.344824 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 161, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.593088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.880869, + "renderer_cpu_ms": 1.877291, + "renderer_readback_cpu_ms": 0.437816, + "simulation_ms": 0.223452, + "snapshot_ms": 0.190148, + "texture_count": 1, + "tick": 161, + "vertices": 576, + "wall_ms": 2.306041 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 162, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587712, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.834311, + "renderer_cpu_ms": 1.829622, + "renderer_readback_cpu_ms": 0.56139, + "simulation_ms": 0.223081, + "snapshot_ms": 0.208613, + "texture_count": 1, + "tick": 162, + "vertices": 576, + "wall_ms": 2.275373 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 163, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588096, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.954999, + "renderer_cpu_ms": 1.951782, + "renderer_readback_cpu_ms": 0.510002, + "simulation_ms": 0.229153, + "snapshot_ms": 0.194897, + "texture_count": 1, + "tick": 163, + "vertices": 576, + "wall_ms": 2.39039 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 164, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.602368, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.911346, + "renderer_cpu_ms": 1.907199, + "renderer_readback_cpu_ms": 0.50392, + "simulation_ms": 0.222119, + "snapshot_ms": 0.193505, + "texture_count": 1, + "tick": 164, + "vertices": 576, + "wall_ms": 2.336979 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 165, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587328, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.830844, + "renderer_cpu_ms": 1.825774, + "renderer_readback_cpu_ms": 0.553234, + "simulation_ms": 0.240353, + "snapshot_ms": 0.197754, + "texture_count": 1, + "tick": 165, + "vertices": 576, + "wall_ms": 2.278408 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 166, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58832, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.955981, + "renderer_cpu_ms": 1.951382, + "renderer_readback_cpu_ms": 0.510934, + "simulation_ms": 0.234352, + "snapshot_ms": 0.194898, + "texture_count": 1, + "tick": 166, + "vertices": 576, + "wall_ms": 2.39581 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 167, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.596256, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.874807, + "renderer_cpu_ms": 1.87077, + "renderer_readback_cpu_ms": 0.45594, + "simulation_ms": 0.214715, + "snapshot_ms": 0.222149, + "texture_count": 1, + "tick": 167, + "vertices": 576, + "wall_ms": 2.32162 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 168, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587392, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.774558, + "renderer_cpu_ms": 1.771302, + "renderer_readback_cpu_ms": 0.525071, + "simulation_ms": 0.245663, + "snapshot_ms": 0.20152, + "texture_count": 1, + "tick": 168, + "vertices": 576, + "wall_ms": 2.23139 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 169, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588064, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.963625, + "renderer_cpu_ms": 1.959197, + "renderer_readback_cpu_ms": 0.540359, + "simulation_ms": 0.226147, + "snapshot_ms": 0.198965, + "texture_count": 1, + "tick": 169, + "vertices": 576, + "wall_ms": 2.397454 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 170, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.616128, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.906187, + "renderer_cpu_ms": 1.903231, + "renderer_readback_cpu_ms": 0.468173, + "simulation_ms": 0.226568, + "snapshot_ms": 0.222189, + "texture_count": 1, + "tick": 170, + "vertices": 576, + "wall_ms": 2.365122 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 171, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.777624, + "renderer_cpu_ms": 1.773446, + "renderer_readback_cpu_ms": 0.475186, + "simulation_ms": 0.212852, + "snapshot_ms": 0.186431, + "texture_count": 1, + "tick": 171, + "vertices": 576, + "wall_ms": 2.186245 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 172, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.915053, + "renderer_cpu_ms": 1.912177, + "renderer_readback_cpu_ms": 0.500024, + "simulation_ms": 0.199627, + "snapshot_ms": 0.189026, + "texture_count": 1, + "tick": 172, + "vertices": 576, + "wall_ms": 2.313805 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 173, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.599584, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.846234, + "renderer_cpu_ms": 1.843107, + "renderer_readback_cpu_ms": 0.444368, + "simulation_ms": 0.223441, + "snapshot_ms": 0.195119, + "texture_count": 1, + "tick": 173, + "vertices": 576, + "wall_ms": 2.274051 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 174, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.822348, + "renderer_cpu_ms": 1.817879, + "renderer_readback_cpu_ms": 0.560137, + "simulation_ms": 0.22285, + "snapshot_ms": 0.184077, + "texture_count": 1, + "tick": 174, + "vertices": 576, + "wall_ms": 2.239765 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 175, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587488, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.04583, + "renderer_cpu_ms": 2.042162, + "renderer_readback_cpu_ms": 0.558003, + "simulation_ms": 0.226287, + "snapshot_ms": 0.189307, + "texture_count": 1, + "tick": 175, + "vertices": 576, + "wall_ms": 2.470962 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 176, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587264, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.946352, + "renderer_cpu_ms": 1.942585, + "renderer_readback_cpu_ms": 0.496978, + "simulation_ms": 0.224363, + "snapshot_ms": 0.22247, + "texture_count": 1, + "tick": 176, + "vertices": 576, + "wall_ms": 2.405208 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 177, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.60448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.845021, + "renderer_cpu_ms": 1.841103, + "renderer_readback_cpu_ms": 0.479234, + "simulation_ms": 0.228481, + "snapshot_ms": 0.194887, + "texture_count": 1, + "tick": 177, + "vertices": 576, + "wall_ms": 2.280422 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 178, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587328, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.941573, + "renderer_cpu_ms": 1.938357, + "renderer_readback_cpu_ms": 0.456712, + "simulation_ms": 0.230785, + "snapshot_ms": 0.192033, + "texture_count": 1, + "tick": 178, + "vertices": 576, + "wall_ms": 2.373679 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 179, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587744, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.159404, + "renderer_cpu_ms": 2.153423, + "renderer_readback_cpu_ms": 0.693458, + "simulation_ms": 0.269298, + "snapshot_ms": 0.178276, + "texture_count": 1, + "tick": 179, + "vertices": 576, + "wall_ms": 2.615394 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 180, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.589088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.341838, + "renderer_cpu_ms": 2.338512, + "renderer_readback_cpu_ms": 0.624648, + "simulation_ms": 0.296579, + "snapshot_ms": 0.263958, + "texture_count": 1, + "tick": 180, + "vertices": 576, + "wall_ms": 2.916182 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 181, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587104, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.059515, + "renderer_cpu_ms": 2.054957, + "renderer_readback_cpu_ms": 0.51951, + "simulation_ms": 0.283365, + "snapshot_ms": 0.253919, + "texture_count": 1, + "tick": 181, + "vertices": 576, + "wall_ms": 2.607048 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 182, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587456, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.955429, + "renderer_cpu_ms": 1.950741, + "renderer_readback_cpu_ms": 0.5937, + "simulation_ms": 0.27006, + "snapshot_ms": 0.23323, + "texture_count": 1, + "tick": 182, + "vertices": 576, + "wall_ms": 2.467976 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 183, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.589088, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.050538, + "renderer_cpu_ms": 2.046802, + "renderer_readback_cpu_ms": 0.516404, + "simulation_ms": 0.298573, + "snapshot_ms": 0.2385, + "texture_count": 1, + "tick": 183, + "vertices": 576, + "wall_ms": 2.596849 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 184, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.011746, + "renderer_cpu_ms": 2.007688, + "renderer_readback_cpu_ms": 0.494723, + "simulation_ms": 0.282554, + "snapshot_ms": 0.236215, + "texture_count": 1, + "tick": 184, + "vertices": 576, + "wall_ms": 2.540153 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 185, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587808, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.282957, + "renderer_cpu_ms": 2.278959, + "renderer_readback_cpu_ms": 0.485406, + "simulation_ms": 0.33349, + "snapshot_ms": 0.2387, + "texture_count": 1, + "tick": 185, + "vertices": 576, + "wall_ms": 2.865145 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 186, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587584, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.203086, + "renderer_cpu_ms": 2.198087, + "renderer_readback_cpu_ms": 0.560878, + "simulation_ms": 0.298313, + "snapshot_ms": 0.242418, + "texture_count": 1, + "tick": 186, + "vertices": 576, + "wall_ms": 2.754016 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 187, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586912, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.953125, + "renderer_cpu_ms": 1.948456, + "renderer_readback_cpu_ms": 0.573572, + "simulation_ms": 0.283926, + "snapshot_ms": 0.260411, + "texture_count": 1, + "tick": 187, + "vertices": 576, + "wall_ms": 2.506168 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 188, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.591072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.058584, + "renderer_cpu_ms": 2.054076, + "renderer_readback_cpu_ms": 0.530591, + "simulation_ms": 0.278094, + "snapshot_ms": 0.239933, + "texture_count": 1, + "tick": 188, + "vertices": 576, + "wall_ms": 2.588003 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 189, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.608768, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.021554, + "renderer_cpu_ms": 2.017045, + "renderer_readback_cpu_ms": 0.526303, + "simulation_ms": 0.287703, + "snapshot_ms": 0.23873, + "texture_count": 1, + "tick": 189, + "vertices": 576, + "wall_ms": 2.558808 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 190, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.585856, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.845161, + "renderer_cpu_ms": 1.841945, + "renderer_readback_cpu_ms": 0.440972, + "simulation_ms": 0.269769, + "snapshot_ms": 0.235103, + "texture_count": 1, + "tick": 190, + "vertices": 576, + "wall_ms": 2.359742 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 191, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588192, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.860069, + "renderer_cpu_ms": 1.855701, + "renderer_readback_cpu_ms": 0.506837, + "simulation_ms": 0.214955, + "snapshot_ms": 0.16416, + "texture_count": 1, + "tick": 191, + "vertices": 576, + "wall_ms": 2.249434 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 192, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.59312, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.833078, + "renderer_cpu_ms": 1.829372, + "renderer_readback_cpu_ms": 0.4505, + "simulation_ms": 0.195299, + "snapshot_ms": 0.165111, + "texture_count": 1, + "tick": 192, + "vertices": 576, + "wall_ms": 2.204028 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 193, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.703935, + "renderer_cpu_ms": 1.700488, + "renderer_readback_cpu_ms": 0.495245, + "simulation_ms": 0.194026, + "snapshot_ms": 0.169289, + "texture_count": 1, + "tick": 193, + "vertices": 576, + "wall_ms": 2.075937 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 194, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.069595, + "renderer_cpu_ms": 2.064835, + "renderer_readback_cpu_ms": 0.558474, + "simulation_ms": 0.284347, + "snapshot_ms": 0.257285, + "texture_count": 1, + "tick": 194, + "vertices": 576, + "wall_ms": 2.622688 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 195, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586944, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.967893, + "renderer_cpu_ms": 1.964416, + "renderer_readback_cpu_ms": 0.438066, + "simulation_ms": 0.278916, + "snapshot_ms": 0.235955, + "texture_count": 1, + "tick": 195, + "vertices": 576, + "wall_ms": 2.493053 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 196, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.744311, + "renderer_cpu_ms": 1.741135, + "renderer_readback_cpu_ms": 0.481369, + "simulation_ms": 0.225425, + "snapshot_ms": 0.197813, + "texture_count": 1, + "tick": 196, + "vertices": 576, + "wall_ms": 2.175946 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 197, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588128, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.883564, + "renderer_cpu_ms": 1.879456, + "renderer_readback_cpu_ms": 0.495164, + "simulation_ms": 0.210367, + "snapshot_ms": 0.165662, + "texture_count": 1, + "tick": 197, + "vertices": 576, + "wall_ms": 2.270454 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 198, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.600448, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.877924, + "renderer_cpu_ms": 1.874236, + "renderer_readback_cpu_ms": 0.471239, + "simulation_ms": 0.197162, + "snapshot_ms": 0.165252, + "texture_count": 1, + "tick": 198, + "vertices": 576, + "wall_ms": 2.250176 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 199, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586848, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.755632, + "renderer_cpu_ms": 1.751595, + "renderer_readback_cpu_ms": 0.484795, + "simulation_ms": 0.193354, + "snapshot_ms": 0.170873, + "texture_count": 1, + "tick": 199, + "vertices": 576, + "wall_ms": 2.129267 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 200, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.589056, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.854038, + "renderer_cpu_ms": 1.850341, + "renderer_readback_cpu_ms": 0.505233, + "simulation_ms": 0.188495, + "snapshot_ms": 0.206991, + "texture_count": 1, + "tick": 200, + "vertices": 576, + "wall_ms": 2.258812 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 201, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.598496, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.822028, + "renderer_cpu_ms": 1.818701, + "renderer_readback_cpu_ms": 0.449969, + "simulation_ms": 0.236746, + "snapshot_ms": 0.172556, + "texture_count": 1, + "tick": 201, + "vertices": 576, + "wall_ms": 2.241158 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 202, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587616, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.815045, + "renderer_cpu_ms": 1.811047, + "renderer_readback_cpu_ms": 0.523087, + "simulation_ms": 0.191992, + "snapshot_ms": 0.166634, + "texture_count": 1, + "tick": 202, + "vertices": 576, + "wall_ms": 2.182348 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 203, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.060668, + "renderer_cpu_ms": 2.055087, + "renderer_readback_cpu_ms": 0.580465, + "simulation_ms": 0.279136, + "snapshot_ms": 0.209987, + "texture_count": 1, + "tick": 203, + "vertices": 576, + "wall_ms": 2.561132 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 204, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.601024, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.864648, + "renderer_cpu_ms": 1.861472, + "renderer_readback_cpu_ms": 0.465849, + "simulation_ms": 0.198414, + "snapshot_ms": 0.170322, + "texture_count": 1, + "tick": 204, + "vertices": 576, + "wall_ms": 2.243183 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 205, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587232, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.794876, + "renderer_cpu_ms": 1.79129, + "renderer_readback_cpu_ms": 0.496146, + "simulation_ms": 0.189537, + "snapshot_ms": 0.166414, + "texture_count": 1, + "tick": 205, + "vertices": 576, + "wall_ms": 2.159985 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 206, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588928, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.832738, + "renderer_cpu_ms": 1.829752, + "renderer_readback_cpu_ms": 0.456481, + "simulation_ms": 0.193515, + "snapshot_ms": 0.170242, + "texture_count": 1, + "tick": 206, + "vertices": 576, + "wall_ms": 2.205711 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 207, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.603968, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.780719, + "renderer_cpu_ms": 1.777103, + "renderer_readback_cpu_ms": 0.435592, + "simulation_ms": 0.216078, + "snapshot_ms": 0.166945, + "texture_count": 1, + "tick": 207, + "vertices": 576, + "wall_ms": 2.174052 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 208, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58688, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.933157, + "renderer_cpu_ms": 1.927908, + "renderer_readback_cpu_ms": 0.673822, + "simulation_ms": 0.194857, + "snapshot_ms": 0.166444, + "texture_count": 1, + "tick": 208, + "vertices": 576, + "wall_ms": 2.318214 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 209, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.588032, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.341487, + "renderer_cpu_ms": 2.336308, + "renderer_readback_cpu_ms": 0.726521, + "simulation_ms": 0.307259, + "snapshot_ms": 0.238961, + "texture_count": 1, + "tick": 209, + "vertices": 576, + "wall_ms": 2.899821 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 210, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.07772, + "renderer_cpu_ms": 2.074193, + "renderer_readback_cpu_ms": 0.520872, + "simulation_ms": 0.300146, + "snapshot_ms": 0.25971, + "texture_count": 1, + "tick": 210, + "vertices": 576, + "wall_ms": 2.648747 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 211, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.964908, + "renderer_cpu_ms": 1.96137, + "renderer_readback_cpu_ms": 0.452073, + "simulation_ms": 0.223352, + "snapshot_ms": 0.181993, + "texture_count": 1, + "tick": 211, + "vertices": 576, + "wall_ms": 2.413494 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 212, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58672, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.127775, + "renderer_cpu_ms": 2.123346, + "renderer_readback_cpu_ms": 0.442324, + "simulation_ms": 0.213593, + "snapshot_ms": 0.180591, + "texture_count": 1, + "tick": 212, + "vertices": 576, + "wall_ms": 2.531586 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 213, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.830925, + "renderer_cpu_ms": 1.827267, + "renderer_readback_cpu_ms": 0.513218, + "simulation_ms": 0.223522, + "snapshot_ms": 0.195459, + "texture_count": 1, + "tick": 213, + "vertices": 576, + "wall_ms": 2.260996 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 214, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587936, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.932456, + "renderer_cpu_ms": 1.928448, + "renderer_readback_cpu_ms": 0.495335, + "simulation_ms": 0.23312, + "snapshot_ms": 0.201059, + "texture_count": 1, + "tick": 214, + "vertices": 576, + "wall_ms": 2.378397 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 215, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.5872, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.905075, + "renderer_cpu_ms": 1.902389, + "renderer_readback_cpu_ms": 0.485857, + "simulation_ms": 0.224404, + "snapshot_ms": 0.21186, + "texture_count": 1, + "tick": 215, + "vertices": 576, + "wall_ms": 2.351868 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 216, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586688, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.799224, + "renderer_cpu_ms": 1.796359, + "renderer_readback_cpu_ms": 0.507578, + "simulation_ms": 0.219173, + "snapshot_ms": 0.19577, + "texture_count": 1, + "tick": 216, + "vertices": 576, + "wall_ms": 2.224286 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 217, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.88687, + "renderer_cpu_ms": 1.883464, + "renderer_readback_cpu_ms": 0.466059, + "simulation_ms": 0.218702, + "snapshot_ms": 0.193135, + "texture_count": 1, + "tick": 217, + "vertices": 576, + "wall_ms": 2.310128 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 218, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587616, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.837216, + "renderer_cpu_ms": 1.834812, + "renderer_readback_cpu_ms": 0.448136, + "simulation_ms": 0.215196, + "snapshot_ms": 0.215506, + "texture_count": 1, + "tick": 218, + "vertices": 576, + "wall_ms": 2.277797 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 219, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.696231, + "renderer_cpu_ms": 1.693705, + "renderer_readback_cpu_ms": 0.485255, + "simulation_ms": 0.209716, + "snapshot_ms": 0.180481, + "texture_count": 1, + "tick": 219, + "vertices": 576, + "wall_ms": 2.095484 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 220, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587808, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.932526, + "renderer_cpu_ms": 1.928548, + "renderer_readback_cpu_ms": 0.458816, + "simulation_ms": 0.191782, + "snapshot_ms": 0.168728, + "texture_count": 1, + "tick": 220, + "vertices": 576, + "wall_ms": 2.301663 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 221, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587168, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.879867, + "renderer_cpu_ms": 1.87638, + "renderer_readback_cpu_ms": 0.401247, + "simulation_ms": 0.223642, + "snapshot_ms": 0.183326, + "texture_count": 1, + "tick": 221, + "vertices": 576, + "wall_ms": 2.297896 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 222, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586528, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.727439, + "renderer_cpu_ms": 1.724042, + "renderer_readback_cpu_ms": 0.508379, + "simulation_ms": 0.239662, + "snapshot_ms": 0.186431, + "texture_count": 1, + "tick": 222, + "vertices": 576, + "wall_ms": 2.165285 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 223, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587808, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.908741, + "renderer_cpu_ms": 1.904804, + "renderer_readback_cpu_ms": 0.468614, + "simulation_ms": 0.220707, + "snapshot_ms": 0.188375, + "texture_count": 1, + "tick": 223, + "vertices": 576, + "wall_ms": 2.329435 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 224, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586496, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.939599, + "renderer_cpu_ms": 1.937115, + "renderer_readback_cpu_ms": 0.414472, + "simulation_ms": 0.204766, + "snapshot_ms": 0.181302, + "texture_count": 1, + "tick": 224, + "vertices": 576, + "wall_ms": 2.334665 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 225, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586976, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.71194, + "renderer_cpu_ms": 1.708724, + "renderer_readback_cpu_ms": 0.483222, + "simulation_ms": 0.211879, + "snapshot_ms": 0.178968, + "texture_count": 1, + "tick": 225, + "vertices": 576, + "wall_ms": 2.112356 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 226, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.585984, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.877362, + "renderer_cpu_ms": 1.872713, + "renderer_readback_cpu_ms": 0.457744, + "simulation_ms": 0.234643, + "snapshot_ms": 0.192283, + "texture_count": 1, + "tick": 226, + "vertices": 576, + "wall_ms": 2.313315 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 227, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.997899, + "renderer_cpu_ms": 1.994754, + "renderer_readback_cpu_ms": 0.444308, + "simulation_ms": 0.218903, + "snapshot_ms": 0.183626, + "texture_count": 1, + "tick": 227, + "vertices": 576, + "wall_ms": 2.41149 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 228, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586592, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.724624, + "renderer_cpu_ms": 1.721057, + "renderer_readback_cpu_ms": 0.49279, + "simulation_ms": 0.205187, + "snapshot_ms": 0.198595, + "texture_count": 1, + "tick": 228, + "vertices": 576, + "wall_ms": 2.140299 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 229, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58704, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.883173, + "renderer_cpu_ms": 1.880197, + "renderer_readback_cpu_ms": 0.468945, + "simulation_ms": 0.230074, + "snapshot_ms": 0.181071, + "texture_count": 1, + "tick": 229, + "vertices": 576, + "wall_ms": 2.305199 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 230, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.977401, + "renderer_cpu_ms": 1.973563, + "renderer_readback_cpu_ms": 0.426074, + "simulation_ms": 0.220856, + "snapshot_ms": 0.191972, + "texture_count": 1, + "tick": 230, + "vertices": 576, + "wall_ms": 2.402032 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 231, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586848, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.773086, + "renderer_cpu_ms": 1.769288, + "renderer_readback_cpu_ms": 0.468895, + "simulation_ms": 0.224994, + "snapshot_ms": 0.203765, + "texture_count": 1, + "tick": 231, + "vertices": 576, + "wall_ms": 2.213326 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 232, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586784, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.911106, + "renderer_cpu_ms": 1.90786, + "renderer_readback_cpu_ms": 0.488101, + "simulation_ms": 0.219504, + "snapshot_ms": 0.189438, + "texture_count": 1, + "tick": 232, + "vertices": 576, + "wall_ms": 2.350334 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 233, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587072, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.887622, + "renderer_cpu_ms": 1.884486, + "renderer_readback_cpu_ms": 0.458134, + "simulation_ms": 0.210697, + "snapshot_ms": 0.187775, + "texture_count": 1, + "tick": 233, + "vertices": 576, + "wall_ms": 2.296243 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 234, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.586656, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.810716, + "renderer_cpu_ms": 1.805937, + "renderer_readback_cpu_ms": 0.497659, + "simulation_ms": 0.222279, + "snapshot_ms": 0.211579, + "texture_count": 1, + "tick": 234, + "vertices": 576, + "wall_ms": 2.25813 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 235, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587744, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.938647, + "renderer_cpu_ms": 1.935181, + "renderer_readback_cpu_ms": 0.446813, + "simulation_ms": 0.247477, + "snapshot_ms": 0.220516, + "texture_count": 1, + "tick": 235, + "vertices": 576, + "wall_ms": 2.420737 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 236, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587616, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.888803, + "renderer_cpu_ms": 1.884536, + "renderer_readback_cpu_ms": 0.467061, + "simulation_ms": 0.204436, + "snapshot_ms": 0.173998, + "texture_count": 1, + "tick": 236, + "vertices": 576, + "wall_ms": 2.277697 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 237, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587168, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.773005, + "renderer_cpu_ms": 1.767846, + "renderer_readback_cpu_ms": 0.531533, + "simulation_ms": 0.247687, + "snapshot_ms": 0.183887, + "texture_count": 1, + "tick": 237, + "vertices": 576, + "wall_ms": 2.214418 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 238, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.594304, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 3.483813, + "renderer_cpu_ms": 3.478934, + "renderer_readback_cpu_ms": 0.569865, + "simulation_ms": 0.234753, + "snapshot_ms": 0.204245, + "texture_count": 1, + "tick": 238, + "vertices": 576, + "wall_ms": 3.935796 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 239, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.587328, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 1.938507, + "renderer_cpu_ms": 1.935281, + "renderer_readback_cpu_ms": 0.471349, + "simulation_ms": 0.199246, + "snapshot_ms": 0.170822, + "texture_count": 1, + "tick": 239, + "vertices": 576, + "wall_ms": 2.318875 + }, + { + "draw_calls": 31, + "dropped_simulation_seconds": 0.0, + "fixed_ticks": 1, + "frame": 240, + "gpu_allocated_bytes": 15787008, + "gpu_ms": 0.58736, + "interpolation_alpha": 0.0, + "physics_debug": false, + "render_call_ms": 2.108167, + "renderer_cpu_ms": 2.10422, + "renderer_readback_cpu_ms": 0.441904, + "simulation_ms": 0.195379, + "snapshot_ms": 0.170962, + "texture_count": 1, + "tick": 240, + "vertices": 576, + "wall_ms": 2.484607 + } + ], + "simulation_mode": "synthetic_fixed_timestep", + "startup_ms": { + "main_to_first_frame": 260.801383, + "renderer_initialization": 252.813953, + "scene_read": 0.606966, + "world_initialization": 0.764282 + }, + "summary_ms": { + "gpu": { + "max": 0.624064, + "min": 0.58432, + "p50": 0.58752, + "p95": 0.600448, + "samples": 240 + }, + "render_call": { + "max": 4.807059, + "min": 1.644403, + "p50": 1.884165, + "p95": 2.179593, + "samples": 240 + }, + "renderer_cpu": { + "max": 4.803463, + "min": 1.626759, + "p50": 1.880197, + "p95": 2.174082, + "samples": 240 + }, + "renderer_readback_cpu": { + "max": 2.729419, + "min": 0.398812, + "p50": 0.468945, + "p95": 0.608337, + "samples": 240 + }, + "simulation": { + "max": 0.441703, + "min": 0.185701, + "p50": 0.213223, + "p95": 0.300146, + "samples": 240 + }, + "snapshot": { + "max": 0.740007, + "min": 0.162066, + "p50": 0.181302, + "p95": 0.257285, + "samples": 240 + }, + "wall": { + "max": 5.902556, + "min": 2.040269, + "p50": 2.298847, + "p95": 2.732225, + "samples": 240 + } + }, + "timing_notes": "Durations use steady_clock wall time, not thread CPU usage. render_call/renderer_cpu include GPU waits, readback and window presentation. renderer_readback_cpu measures map/copy/unmap wall time within that call. GPU timestamps cover submitted rendering, not CPU work. A missing/zero GPU timestamp is null. Frame durations exclude profile bookkeeping and final capture/profile file writes. Startup begins at main(), excluding OS loader/launcher.", + "validation_enabled": true, + "validation_errors": 0, + "version": 1, + "width": 1280 +} diff --git a/docs/validation/linux-release-2026-09-18/collect-3d.png b/docs/validation/linux-release-2026-09-18/collect-3d.png new file mode 100644 index 0000000..ca55e0b Binary files /dev/null and b/docs/validation/linux-release-2026-09-18/collect-3d.png differ diff --git a/docs/validation/linux-release-2026-09-18/evidence.json b/docs/validation/linux-release-2026-09-18/evidence.json new file mode 100644 index 0000000..25b94e4 --- /dev/null +++ b/docs/validation/linux-release-2026-09-18/evidence.json @@ -0,0 +1,312 @@ +{ + "format": "faset.validation-evidence", + "version": 1, + "status": "passed", + "scope": "Linux relocated Release game export and offscreen small-scene measurement", + "source_checkout": { + "reference_commit": "5ac4db438dae5ee28e5619e62cf8d6647beecd52", + "clean_commit_match": false, + "description": "Shared working tree around 5ac4db4 plus in-progress checkpoint 4 work; builds were not made from a frozen clean commit. Exact exported binary/shader hashes are retained in each manifest." + }, + "export_started_utc": "2026-09-18T01:46:06.267995+00:00", + "export_finished_utc": "2026-09-18T01:49:50.149934+00:00", + "host": { + "os": "Ubuntu 26.04 LTS", + "kernel": "7.0.0-31-generic x86_64", + "cpu": "AMD Ryzen 7 1700 Eight-Core Processor (8 cores,16 threads)", + "system_memory_bytes": 33571037184, + "gpu": "NVIDIA GeForce RTX 2080 Ti", + "gpu_memory_mib": 11264, + "nvidia_driver": "595.84" + }, + "configuration": { + "build": "Release", + "compiler": "Clang 21.1.8", + "cxx_flags": "-O3 -DNDEBUG", + "cmake": "4.2.3", + "api": "Vulkan 1.3", + "resolution": [ + 1280, + 720 + ], + "presentation": "offscreen", + "validation_enabled": true, + "physics_debug": false, + "frames_per_final_profile": 240, + "fixed_delta_seconds": 0.016666666666666666, + "simulation": "synthetic fixed timestep, no player input", + "sampling": "nearest rank, all completed frames, cold first frame retained" + }, + "verification": { + "tool": "tools/verify_playable_exports.py", + "project_copies_and_relocated_binaries_include": "Faset Café 世界", + "steps": [ + "Copy exact checked-in sample project inputs", + "Real Editor import of 3D Blender arch bundle", + "Real Editor Release export", + "Verify every package file size and SHA256", + "Copy generation outside engine/source trees", + "Hide disposable source-project paths", + "Launch Player from unrelated empty cwd without scene/assets arguments", + "CPU --validate then 120-frame offscreen capture and profile", + "Additional 240-frame no-debug profile of each same relocated executable" + ], + "validation_errors": 0 + }, + "projects": [ + { + "name": "collect-2d", + "dimension": 2, + "generation": "48ec8546-3750-463f-9c4c-f76c3b933256", + "build_fingerprint": "5b1996b0fe44eeaa01ee1fd581f9f063f8c84147128e896541d27207dbab9aaa", + "scene_hash": "05ab2c3e5b152ab25867976c47635961f440f41d8cd0408732eb2d8d5b7800a8", + "asset_generations": {}, + "source_inputs": [ + { + "path": ".gitignore", + "sha256": "53aa4d7124d4c8d93b8d7c4cb470b6c82b844e019a660d6243ffeadc50602de1" + }, + { + "path": "README.md", + "sha256": "d6c35206b10ee28efd5cc02da77a3ce9b6e1f8ce085793150bbf2f23bd925d65" + }, + { + "path": "Scenes/main.scene.json", + "sha256": "be48a6d21385e717b6c8ebf9a3b4322eebbeff6bb067428e089ab5214be79854" + }, + { + "path": "Scripts/Gameplay.cpp", + "sha256": "6b581b24da613e1915819de0a0f0ea820dd3333efa62c4b479a9ea7ffdbc6bcc" + }, + { + "path": "Scripts/Gameplay.hpp", + "sha256": "4121587182ed4f74901b1c0757c53daadbf309af1d6bb5dd87fef142130ccbbe" + }, + { + "path": "project.faset.json", + "sha256": "36e2acd32210f5489372a2331346a3294ead2395f0713e3e5fd2da4e83263689" + } + ], + "package_file_count": 17, + "source_project_paths_unavailable": true, + "standalone_validation": "passed", + "standalone_render_frames": 120, + "capture": { + "width": 1280, + "height": 720, + "sampled_colors": 7, + "sha256": "f9d74067d87c1ebf30c6db372a26d93b7285791c8cb0307a24af5adb23bf201a" + }, + "final_profile": "collect-2d-profile-240.json", + "profile_recorded_at_utc": "2026-09-18T01:51:24.599491+00:00", + "gpu_allocated_bytes": { + "min": 15750288, + "max": 15750288 + }, + "draw_calls": [ + 11 + ], + "vertices": [ + 66 + ], + "texture_count": [ + 1 + ], + "summary_ms": { + "gpu": { + "max": 0.58912, + "min": 0.563616, + "p50": 0.564832, + "p95": 0.57856, + "samples": 240 + }, + "render_call": { + "max": 5.705444, + "min": 1.581003, + "p50": 1.865269, + "p95": 2.22086, + "samples": 240 + }, + "renderer_cpu": { + "max": 5.701056, + "min": 1.578639, + "p50": 1.861723, + "p95": 2.216031, + "samples": 240 + }, + "renderer_readback_cpu": { + "max": 2.763424, + "min": 0.432917, + "p50": 0.509912, + "p95": 0.632894, + "samples": 240 + }, + "simulation": { + "max": 0.513789, + "min": 0.144743, + "p50": 0.200708, + "p95": 0.258247, + "samples": 240 + }, + "snapshot": { + "max": 0.25444, + "min": 0.127029, + "p50": 0.170341, + "p95": 0.214946, + "samples": 240 + }, + "wall": { + "max": 6.089358, + "min": 1.879436, + "p50": 2.233955, + "p95": 2.684014, + "samples": 240 + } + } + }, + { + "name": "collect-3d", + "dimension": 3, + "generation": "17281e17-9f4f-44f0-8821-6e0071e9a45b", + "build_fingerprint": "5a516f5c4bded8f8e3f027b3dd2812a34b7f40b37e281c21ce87d7586fff88e4", + "scene_hash": "6510f994ee01af5823614911fcdeff0fd433846d937ff1a65d5bf5fb08e45e8f", + "asset_generations": { + "5832763b-3ed0-44d6-9088-0b524f196a91": "4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2" + }, + "source_inputs": [ + { + "path": ".gitignore", + "sha256": "53aa4d7124d4c8d93b8d7c4cb470b6c82b844e019a660d6243ffeadc50602de1" + }, + { + "path": "Assets/exit-arch/create.py", + "sha256": "9c7291124fb1498a3f992ef74afc39cd98f9cc3e2ff4687c633bece85d580905" + }, + { + "path": "Assets/exit-arch/manifest.json", + "sha256": "7a13347adc89761fb02ebeab3c58ba87f319cd62f53ec87a3bb61bcd066ffa55" + }, + { + "path": "Assets/exit-arch/manifest.json.faset-import.json", + "sha256": "ef4eef28a18a0cc40a40df75db34c97faa5d783b891265f862633f3aec514afc" + }, + { + "path": "Assets/exit-arch/payload/ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3.glb", + "sha256": "ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3" + }, + { + "path": "Assets/exit-arch/source.blend", + "sha256": "ff2676bec97ab778e531e87a185ad716049ea3b05b4a4f0027a6345737735035" + }, + { + "path": "README.md", + "sha256": "2df5ca4df47ab5d3d05d41508201296d4ad5ec876245f19f061767ade6a30e8f" + }, + { + "path": "Scenes/main.scene.json", + "sha256": "61b65baf79e52f07ac386a7f98acf985a1b09e88770cdde37ef461dc94b9dcd7" + }, + { + "path": "Scripts/Extensions/Beacon.hpp", + "sha256": "c60d56161eead16f541835053ad32ac3333383d83015709c060b7d45e0af5a97" + }, + { + "path": "Scripts/Gameplay.cpp", + "sha256": "5d419786a6861d7f14ed4eca54634694d8cd43be5dbbd60a2c6790db665c3a6a" + }, + { + "path": "Scripts/Gameplay.hpp", + "sha256": "4121587182ed4f74901b1c0757c53daadbf309af1d6bb5dd87fef142130ccbbe" + }, + { + "path": "project.faset.json", + "sha256": "43d273b1054ec9069369073f54cc1fc7efb3d6b699dcd342c552a2dc2ef852ef" + } + ], + "package_file_count": 22, + "source_project_paths_unavailable": true, + "standalone_validation": "passed", + "standalone_render_frames": 120, + "capture": { + "width": 1280, + "height": 720, + "sampled_colors": 42, + "sha256": "914060401de93d08b33bf09a1e210859e491a0060c331e5e2044f517356f4980" + }, + "final_profile": "collect-3d-profile-240.json", + "profile_recorded_at_utc": "2026-09-18T01:51:25.501500+00:00", + "gpu_allocated_bytes": { + "min": 15787008, + "max": 15787008 + }, + "draw_calls": [ + 31 + ], + "vertices": [ + 576 + ], + "texture_count": [ + 1 + ], + "summary_ms": { + "gpu": { + "max": 0.624064, + "min": 0.58432, + "p50": 0.58752, + "p95": 0.600448, + "samples": 240 + }, + "render_call": { + "max": 4.807059, + "min": 1.644403, + "p50": 1.884165, + "p95": 2.179593, + "samples": 240 + }, + "renderer_cpu": { + "max": 4.803463, + "min": 1.626759, + "p50": 1.880197, + "p95": 2.174082, + "samples": 240 + }, + "renderer_readback_cpu": { + "max": 2.729419, + "min": 0.398812, + "p50": 0.468945, + "p95": 0.608337, + "samples": 240 + }, + "simulation": { + "max": 0.441703, + "min": 0.185701, + "p50": 0.213223, + "p95": 0.300146, + "samples": 240 + }, + "snapshot": { + "max": 0.740007, + "min": 0.162066, + "p50": 0.181302, + "p95": 0.257285, + "samples": 240 + }, + "wall": { + "max": 5.902556, + "min": 2.040269, + "p50": 2.298847, + "p95": 2.732225, + "samples": 240 + } + } + } + ], + "evidence_sha256": { + "collect-2d-profile-240.json": "0141cb2ad6d31394b542d575dce3409fef82ff2e77fe91eb4a7183811471e36f", + "collect-3d-profile-240.json": "bedf3a68df03c1af42bdb4d3b416d930c6e92fa6e99033c63ea9b17ae977c8c6", + "collect-2d-manifest.json": "83a0c8a58dae07376c0db58ca58b115d9ecedb245bcba88bdcd5429796ba5be9", + "collect-3d-manifest.json": "6374a6af5618a6c613b09978b2bf42300b1565808ded21bf9c2b82444027f252", + "collect-2d.png": "c380583f2fd8bc09c5b0e1c6e755c35c3d64b5385ed66177b04d1bbdb532b236", + "collect-3d.png": "98941a2004c3345da336294fa0e1b9e363922fc581c7a8612652ecbb0d764c07" + } +} diff --git a/docs/validation/native-window-linux-2026-09-18.json b/docs/validation/native-window-linux-2026-09-18.json new file mode 100644 index 0000000..bb0d4c2 --- /dev/null +++ b/docs/validation/native-window-linux-2026-09-18.json @@ -0,0 +1,50 @@ +{ + "format": "faset.native-window-validation", + "version": 1, + "date": "2026-09-18", + "platform": "Linux, KDE Wayland session and isolated Xvfb", + "source_checkout": "Working tree around5ac4db4 plus checkpoint4 changes; not an exact clean-commit result", + "fixture": "tests/render_window_tests.cpp", + "cases": [ + { + "surface": "XWayland in the active desktop", + "result": "passed", + "scope": [ + "resize convergence", + "fresh readback while minimized", + "native minimize/restore events", + "live SDL display-scale getter", + "native SDL text input start/stop", + "drawable-pixel input-area conversion checked through SDL_GetTextInputArea" + ], + "clipboard": "Skipped because the existing clipboard advertised non-text payload; no replacement attempted.", + "log": "driver=x11 stage=created\nstage=resized width=360 height=300 resize_events=2\nclipboard=skipped_to_preserve_non_text_payload\ntext_input_area=coordinate_conversion_passed\nstage=minimized fresh_frames=12 minimize_events=1 focus_lost_events=1\nstage=restored restored_events=1 activation_required=0 frames=56 validation_enabled=1 validation_errors=0\n" + }, + { + "surface": "Private Xvfb server, SDL X11 driver", + "result": "clipboard and input-area checks passed; lifecycle skipped", + "scope": [ + "Unicode clipboard roundtrip through Renderer/SDL", + "saved plain-text clipboard restored without logging contents", + "native SDL text-input activation and rectangle conversion" + ], + "limit": "No window manager; minimize request unconfirmed, so this is not minimize/restore acceptance.", + "log": "driver=x11 stage=created\nstage=resized width=360 height=300 resize_events=2\nclipboard=unicode_roundtrip_passed\ntext_input_area=coordinate_conversion_passed\nSKIP: window system declined the minimize request\n" + }, + { + "surface": "Native Wayland in the active desktop", + "result": "partial; restore acceptance unresolved", + "scope": [ + "resize convergence", + "12 fresh readbacks while SDL reported minimized" + ], + "limit": "Compositor did not confirm restore/activation. SDL Wayland may synthesize its minimized flag because xdg-shell has no minimized-state acknowledgement; physical minimization is not independently established." + } + ], + "limits": [ + "No real OS IME composition session was performed.", + "Synthetic text events in other tests do not substitute for real IME acceptance.", + "Windows native clipboard/window behavior requires its own run.", + "No clipboard contents were retained in logs or artifacts." + ] +} diff --git a/docs/validation/offline-linux-2026-09-18.json b/docs/validation/offline-linux-2026-09-18.json new file mode 100644 index 0000000..0ea5323 --- /dev/null +++ b/docs/validation/offline-linux-2026-09-18.json @@ -0,0 +1,12 @@ +{ + "format": "faset.offline-build", + "version": 1, + "revision": "d834cfad67cd81d8c4998b90c16791361ca8c0f8", + "recorded_at_utc": "2026-09-18T01:33:07.301477+00:00", + "seconds": 174.03467636999994, + "returncode": 0, + "network": "fresh user/network namespace; external connection must fail", + "source": "git archive of committed HEAD; no existing build directory", + "inputs": "prefetched archives + Slang; system compiler, SDK and development libraries", + "tests": "full native build, CPU CTests; GPU/window execution verified separately" +} diff --git a/include/faset/assets/asset_pipeline.hpp b/include/faset/assets/asset_pipeline.hpp index e0a5186..fe310a2 100644 --- a/include/faset/assets/asset_pipeline.hpp +++ b/include/faset/assets/asset_pipeline.hpp @@ -43,10 +43,13 @@ struct ImportRequest { Json settings = nullptr; // Null restores the sidecar recipe; an object replaces it. // Explicit conflict resolution; false keeps the previous generation active. bool allow_removed_outputs = false; + // Optional review guards: reject a changed candidate or active generation. + // Empty keeps the ordinary import API; the Editor sets both on explicit acceptance. + std::string expected_generation{}, expected_active_generation{}; }; struct ImportResult { ImportStatus status = ImportStatus::failed; - std::string asset_id, generation; + std::string asset_id, generation, previous_generation; std::vector diagnostics; std::vector removed_output_ids; Json manifest; diff --git a/include/faset/core/io.hpp b/include/faset/core/io.hpp index af17402..6786aac 100644 --- a/include/faset/core/io.hpp +++ b/include/faset/core/io.hpp @@ -5,6 +5,19 @@ #include namespace faset { +// Text/JSON/process arguments use UTF-8. Keep filesystem paths in native form +// internally; narrow path constructors/string() depend on the Windows code page. +std::filesystem::path path_from_utf8(std::string_view text); +std::string path_to_utf8(const std::filesystem::path& path); +std::string generic_path_to_utf8(const std::filesystem::path& path); +// Use only at file-I/O boundaries, never for serialized paths or identity comparisons. +// Windows: absolute, normalized extended-length path; other systems: unchanged. +std::filesystem::path native_io_path(const std::filesystem::path& path); +#ifdef _WIN32 +// Thin wmain adapter: preserve the CRT's Unicode argument parsing and pass UTF-8 +// to the shared application entry. No process/global code-page change is needed. +int run_utf8_main(int argc, wchar_t** argv, int (*entry)(int, char**)) noexcept; +#endif std::string new_id(); std::string read_text(const std::filesystem::path& path); Json read_json(const std::filesystem::path& path); diff --git a/include/faset/core/process.hpp b/include/faset/core/process.hpp index 8f8fb89..90f2e43 100644 --- a/include/faset/core/process.hpp +++ b/include/faset/core/process.hpp @@ -8,10 +8,11 @@ namespace faset { struct ProcessOptions { - // First element is the executable. Arguments are passed directly, never through a shell. + // UTF-8 text; the first element is the executable (use path_to_utf8 for paths). + // Arguments are passed directly, never through a shell. std::vector arguments; std::filesystem::path working_directory; - std::map environment; + std::map environment; // UTF-8 names and values. }; struct ProcessPoll { bool running{}; diff --git a/include/faset/render/renderer.hpp b/include/faset/render/renderer.hpp index 3ec9008..bd03d44 100644 --- a/include/faset/render/renderer.hpp +++ b/include/faset/render/renderer.hpp @@ -32,20 +32,23 @@ struct Mesh { std::shared_ptr cube_mesh(); struct Texture; struct DrawItem { - std::shared_ptr mesh; + std::shared_ptr mesh{}; Mat4 model{identity}; Color color{1, 1, 1, 1}; float roughness{0.65f}; float metallic{0.0f}; bool cast_shadow{true}; - std::shared_ptr texture; + std::shared_ptr texture{}; }; struct Sprite { Vec3 position{}; Vec2 size{1, 1}; Color color{1, 1, 1, 1}; float rotation{}; - std::shared_ptr texture; + std::shared_ptr texture{}; + // Higher layers draw later. Within a layer, projected depth is sorted back to front; + // equal-depth sprites retain their submission order. + int layer{}; }; struct Texture { std::uint32_t width{}, height{}; @@ -56,7 +59,7 @@ struct Texture { struct Quad { float x{}, y{}, width{}, height{}; Color color{1, 1, 1, 1}; - std::shared_ptr texture; + std::shared_ptr texture{}; std::array uv_rect{0, 0, 1, 1}; }; struct Text { @@ -88,7 +91,7 @@ struct RendererConfig { bool headless{false}; bool validation{true}; // Optional isolated shader bundle, useful for editor preview and shader reload tests. - std::filesystem::path shader_directory; + std::filesystem::path shader_directory{}; }; struct Event { enum class Type { @@ -141,6 +144,8 @@ class Renderer { std::vector pixels() const; std::uint32_t width() const; std::uint32_t height() const; + // Effective UI scale in drawable pixels; updates when the window changes display. + float display_scale() const; bool should_close() const; const FrameStats& stats() const; void set_title(const std::string&); diff --git a/include/faset/runtime/schema.hpp b/include/faset/runtime/schema.hpp new file mode 100644 index 0000000..c9181ff --- /dev/null +++ b/include/faset/runtime/schema.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include + +namespace faset::runtime { +// Exact native TypeIds. A prefix alone does not make a custom type native. +bool is_builtin_component(std::string_view type) noexcept; + +// Player boundary: require every custom TypeId and its exact positive integer +// version in the linked gameplay schema. Accepts its array or a {types:[...]} +// manifest. Native components always require version 1. A missing version means +// 1 for existing development scenes. Gameplay may not redeclare native TypeIds. +// Does not migrate or change source data. +// Structural/native field checks remain Runtime::load's responsibility; this +// function has no authoring dependency and runs no gameplay callbacks. +void validate_scene_schemas(const nlohmann::json& scene, const nlohmann::json& gameplay_schema); +} // namespace faset::runtime diff --git a/include/faset/ui/ui.hpp b/include/faset/ui/ui.hpp index 6f2f878..9f75617 100644 --- a/include/faset/ui/ui.hpp +++ b/include/faset/ui/ui.hpp @@ -162,6 +162,9 @@ class Context { // edit state. void validate_layout(const Json&) const; void apply_layout(const Json&); + // Layout metrics are logical units; rectangles/events/IME areas are drawable + // pixels. Scale also selects the glyph raster size (supported range .5–4). + // Changing it preserves text/focus and cancels active pointer drags. void layout(float drawable_width, float drawable_height, float dpi_scale = 1); bool handle(const render::Event&); void draw(render::Snapshot&); diff --git a/src/assets/asset_data.cpp b/src/assets/asset_data.cpp index b01b579..686dca0 100644 --- a/src/assets/asset_data.cpp +++ b/src/assets/asset_data.cpp @@ -19,9 +19,9 @@ void valid_id(const std::string& id) { throw std::runtime_error("Invalid AssetId"); } std::vector read_bytes(const fs::path& path) { - std::ifstream file(path, std::ios::binary | std::ios::ate); + std::ifstream file(faset::native_io_path(path), std::ios::binary | std::ios::ate); if (!file) - throw std::runtime_error("Cannot read cooked file: " + path.string()); + throw std::runtime_error("Cannot read cooked file: " + faset::path_to_utf8(path)); auto length = file.tellg(); if (length < 0 || static_cast(length) > 1024ull * 1024 * 1024) throw std::runtime_error("Cooked file exceeds 1 GiB limit"); @@ -42,7 +42,7 @@ Json read_json(const fs::path& path) { fs::path cooked_path(const fs::path& directory, const std::string& name) { if (name.empty()) throw std::runtime_error("Empty cooked path"); - return faset::project_path(directory, fs::path(name)); + return faset::project_path(directory, faset::path_from_utf8(name)); } void validate_generation(const fs::path& directory, const Json& manifest) { if (manifest.at("schema_version") != 1) @@ -142,7 +142,7 @@ CookedAsset AssetStore::load_asset(const std::string& id) const { const auto m = read_json(directory / "manifest.json"); validate_generation(directory, m); if (m.at("asset_id").get() != id || - m.at("generation").get() != directory.filename().string()) + m.at("generation").get() != faset::path_to_utf8(directory.filename())) throw std::runtime_error("Cooked asset identity does not match its generation"); CookedAsset asset; asset.asset_id = m.at("asset_id"); diff --git a/src/assets/asset_pipeline.cpp b/src/assets/asset_pipeline.cpp index 1cbfaf3..f0e551a 100644 --- a/src/assets/asset_pipeline.cpp +++ b/src/assets/asset_pipeline.cpp @@ -1,6 +1,7 @@ #include #include #include +#include // Import validation owns a private decoder; Player's decoder remains a separate // binary boundary. #define STB_IMAGE_IMPLEMENTATION @@ -63,33 +64,33 @@ void valid_id(const std::string& id) { throw std::runtime_error("Invalid AssetId"); } std::vector read_bytes(const fs::path& path) { - std::ifstream file(path, std::ios::binary | std::ios::ate); + std::ifstream file(faset::native_io_path(path), std::ios::binary | std::ios::ate); if (!file) - throw std::runtime_error("Cannot read: " + path.string()); + throw std::runtime_error("Cannot read: " + faset::path_to_utf8(path)); const auto length = file.tellg(); if (length < 0 || static_cast(length) > 1024ull * 1024 * 1024) - throw std::runtime_error("Input exceeds 1 GiB limit: " + path.string()); + throw std::runtime_error("Input exceeds 1 GiB limit: " + faset::path_to_utf8(path)); std::vector data(static_cast(length)); file.seekg(0); if (!data.empty() && !file.read(reinterpret_cast(data.data()), static_cast(data.size()))) - throw std::runtime_error("Short read: " + path.string()); + throw std::runtime_error("Short read: " + faset::path_to_utf8(path)); return data; } void write_bytes(const fs::path& path, const std::vector& data) { fs::create_directories(path.parent_path()); - std::ofstream out(path, std::ios::binary | std::ios::trunc); + std::ofstream out(faset::native_io_path(path), std::ios::binary | std::ios::trunc); if (!out || (!data.empty() && !out.write(reinterpret_cast(data.data()), static_cast(data.size())))) - throw std::runtime_error("Cannot write: " + path.string()); + throw std::runtime_error("Cannot write: " + faset::path_to_utf8(path)); out.close(); if (!out) - throw std::runtime_error("Cannot close: " + path.string()); + throw std::runtime_error("Cannot close: " + faset::path_to_utf8(path)); } Json read_json(const fs::path& path) { - std::ifstream in(path); + std::ifstream in(faset::native_io_path(path)); if (!in) - throw std::runtime_error("Cannot read JSON: " + path.string()); + throw std::runtime_error("Cannot read JSON: " + faset::path_to_utf8(path)); return Json::parse(in); } void write_json(const fs::path& path, const Json& value) { @@ -105,9 +106,10 @@ void atomic_json(const fs::path& path, const Json& value) { try { write_json(temporary, value); #ifdef _WIN32 - if (!MoveFileExW(temporary.c_str(), path.c_str(), + if (!MoveFileExW(faset::native_io_path(temporary).c_str(), + faset::native_io_path(path).c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) - throw std::runtime_error("Atomic replace failed: " + path.string()); + throw std::runtime_error("Atomic replace failed: " + faset::path_to_utf8(path)); #else fs::rename(temporary, path); #endif @@ -177,7 +179,7 @@ std::vector decode_data_uri(const std::string& uri) { fs::path external_path(const fs::path& source, const std::string& uri) { if (uri.find("://") != std::string::npos) throw std::runtime_error("Network URI is not an import dependency: " + uri); - const fs::path relative = uri_decode(uri); + const fs::path relative = faset::path_from_utf8(uri_decode(uri)); if (relative.is_absolute()) throw std::runtime_error("glTF URI must be relative"); return (source.parent_path() / relative).lexically_normal(); @@ -306,13 +308,15 @@ void validate_generation(const fs::path& directory, const Json& manifest) { if (manifest.at("schema_version") != 1) throw std::runtime_error("Unsupported asset manifest version"); for (const auto& file : manifest.at("files")) { - const fs::path relative = file.at("path").get(); - if (relative.is_absolute() || relative.string().find("..") != std::string::npos) + const fs::path relative = faset::path_from_utf8(file.at("path").get()); + if (relative.is_absolute() || + faset::generic_path_to_utf8(relative).find("..") != std::string::npos) throw std::runtime_error("Invalid cooked file path"); auto bytes = read_bytes(directory / relative); if (bytes.size() != file.at("size").get() || hash_bytes(bytes) != file.at("sha256").get()) - throw std::runtime_error("Corrupt cooked file: " + relative.string()); + throw std::runtime_error("Corrupt cooked file: " + + faset::generic_path_to_utf8(relative)); } } Json material_json(const Material& m) { @@ -387,8 +391,9 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob valid_id(bundle_asset_id); bool found_payload = false; for (const auto& file : bundle.at("files")) { - const auto relative = fs::path(file.at("path").get()); - if (relative.is_absolute() || relative.string().find("..") != std::string::npos) + const auto relative = faset::path_from_utf8(file.at("path").get()); + if (relative.is_absolute() || + faset::generic_path_to_utf8(relative).find("..") != std::string::npos) throw std::runtime_error("Invalid bundle payload path"); const auto candidate = logical_source.parent_path() / relative; const auto bytes = read_bytes(candidate); @@ -407,7 +412,8 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob } const auto source_bytes = source == logical_source ? logical_bytes : payload_snapshot; const auto source_hash = hash_bytes(source_bytes); - const auto sidecar = fs::path(logical_source.string() + ".faset-import.json"); + auto sidecar = logical_source; + sidecar += ".faset-import.json"; Json metadata = fs::exists(sidecar) ? read_json(sidecar) : Json::object(); if (!metadata.is_object() || (!metadata.empty() && metadata.value("schema_version", 0) != 1)) @@ -431,14 +437,15 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob Json previous = fs::exists(asset_root / "current.json") ? current_manifest(result.asset_id) : Json(); if (!previous.is_null()) { - const auto previous_source = fs::path(previous.at("source").get()); + const auto previous_source = + faset::path_from_utf8(previous.at("source").get()); if (previous_source != logical_source && fs::exists(previous_source)) throw std::runtime_error("Duplicate AssetId: previous source still exists"); } Dependencies dependencies; CookedAsset asset; asset.asset_id = result.asset_id; - auto extension = source.extension().string(); + auto extension = faset::path_to_utf8(source.extension()); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); const bool standalone_image = @@ -514,7 +521,7 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob data->buffers[i].data_free_method = cgltf_data_free_method_none; } } - if (cgltf_load_buffers(&options, data.get(), source.string().c_str()) != + if (cgltf_load_buffers(&options, data.get(), faset::path_to_utf8(source).c_str()) != cgltf_result_success) throw std::runtime_error("Cannot load glTF buffers"); if (cgltf_validate(data.get()) != cgltf_result_success) @@ -710,8 +717,8 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob Json manifest{{"schema_version", 1}, {"asset_id", result.asset_id}, {"generation", result.generation}, - {"source", logical_source.string()}, - {"payload_source", source.string()}, + {"source", faset::path_to_utf8(logical_source)}, + {"payload_source", faset::path_to_utf8(source)}, {"source_sha256", source_hash}, {"importer", recipe_version}, {"settings", settings}, @@ -772,6 +779,19 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob if (!published_ids.contains(old.get())) result.removed_output_ids.push_back(old.get()); result.manifest = manifest; + result.previous_generation = + previous.is_null() ? "" : previous.at("generation").get(); + if ((!request.expected_generation.empty() && + request.expected_generation != result.generation) || + (!request.expected_active_generation.empty() && + request.expected_active_generation != result.previous_generation)) { + result.status = ImportStatus::conflict; + result.diagnostics.push_back( + "Import changed since review; reimport and review the current removed outputs. " + "Active generation preserved"); + fs::remove_all(stage); + return result; + } if (!result.removed_output_ids.empty() && !request.allow_removed_outputs) { result.status = ImportStatus::conflict; result.diagnostics.push_back( @@ -808,7 +828,7 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob throw Cancelled{}; atomic_json(asset_root / "current.json", {{"schema_version", 1}, {"generation", result.generation}, - {"source", logical_source.string()}}); + {"source", faset::path_to_utf8(logical_source)}}); result.status = ImportStatus::succeeded; job.report(1, "complete"); } catch (const Cancelled&) { @@ -826,15 +846,15 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob } Json AssetPipeline::overrides(const std::string& id) const { const auto source = current_manifest(id).at("source").get(); - const auto path = fs::path(source + ".faset-overrides.json"); + const auto path = faset::path_from_utf8(source + ".faset-overrides.json"); return fs::exists(path) ? read_json(path) : Json::object(); } void AssetPipeline::set_overrides(const std::string& id, const Json& values) { if (!values.is_object()) throw std::runtime_error("Overrides must be an object keyed by stable output IDs"); std::lock_guard lock(writer_mutex); - atomic_json( - fs::path(current_manifest(id).at("source").get() + ".faset-overrides.json"), - values); + atomic_json(faset::path_from_utf8(current_manifest(id).at("source").get() + + ".faset-overrides.json"), + values); } } // namespace faset::assets diff --git a/src/authoring/service.cpp b/src/authoring/service.cpp index ff297a6..ccbd625 100644 --- a/src/authoring/service.cpp +++ b/src/authoring/service.cpp @@ -233,7 +233,7 @@ Json AuthoringService::summary(const State& value, bool include_data) const { {"name", value.data.at("name")}, {"revision", value.revision}, {"dirty", sha256(value.data.dump()) != value.saved_hash}, - {"path", value.path.generic_string()}, + {"path", generic_path_to_utf8(value.path)}, {"can_undo", !value.undo.empty()}, {"can_redo", !value.redo.empty()}}; if (include_data) @@ -245,7 +245,7 @@ void AuthoringService::journal(const State& value) const { (value.data.at("id").get() + ".json")), {{"format", "faset.recovery"}, {"version", 1}, - {"path", value.path.generic_string()}, + {"path", generic_path_to_utf8(value.path)}, {"revision", value.revision}, {"saved_hash", value.saved_hash}, {"disk_hash", value.disk_hash}, @@ -468,7 +468,7 @@ void AuthoringService::apply(Json& scene, const Json& command) { } if (op == "template.source_set") { const auto source = command.at("source").get(); - project_path(root_, source); + project_path(root_, path_from_utf8(source)); (*found)["source"] = source; return; } @@ -593,7 +593,7 @@ Json AuthoringService::recover(const std::string& id, validate_scene(candidate.data, schemas_); require(candidate.data.at("id") == id, "recovery.id", "Recovery ID does not match its document"); - candidate.path = record.at("path").get(); + candidate.path = path_from_utf8(record.at("path").get()); candidate.saved_hash = record.value("saved_hash", std::string()); candidate.disk_hash = record.value("disk_hash", std::string()); candidate.revision = record.value("revision", std::uint64_t(0)); @@ -642,7 +642,7 @@ Json AuthoringService::recovery_documents() const { value.value("saved_hash", std::string())}}); } catch (const std::exception&) { result.push_back({{"error", "Invalid recovery record"}, - {"file", entry.path().filename().string()}}); + {"file", path_to_utf8(entry.path().filename())}}); } } return result; diff --git a/src/core/hash.cpp b/src/core/hash.cpp index 19adefe..b1e8082 100644 --- a/src/core/hash.cpp +++ b/src/core/hash.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -101,8 +102,8 @@ std::string sha256(std::span bytes) { return digest.finish(); } std::string sha256_file(const std::filesystem::path& path) { - std::ifstream stream(path, std::ios::binary); - require(bool(stream), "io.open", "Cannot open file for hashing: " + path.string()); + std::ifstream stream(native_io_path(path), std::ios::binary); + require(bool(stream), "io.open", "Cannot open file for hashing: " + path_to_utf8(path)); Digest digest; std::array buffer{}; while (stream) { @@ -110,7 +111,7 @@ std::string sha256_file(const std::filesystem::path& path) { digest.update( std::as_bytes(std::span(buffer.data(), static_cast(stream.gcount())))); } - require(stream.eof(), "io.read", "Cannot read file for hashing: " + path.string()); + require(stream.eof(), "io.read", "Cannot read file for hashing: " + path_to_utf8(path)); return digest.finish(); } } // namespace faset diff --git a/src/core/io.cpp b/src/core/io.cpp index 9fab6c2..1728cc2 100644 --- a/src/core/io.cpp +++ b/src/core/io.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #ifdef _WIN32 #define NOMINMAX #include @@ -13,6 +15,86 @@ #endif namespace faset { +namespace { +void validate_utf8_path(std::string_view text) { + for (std::size_t i = 0; i < text.size();) { + const auto first = static_cast(text[i++]); + require(first != 0, "path.encoding", "NUL in filesystem path"); + if (first < 0x80) + continue; + unsigned remaining = first >= 0xc2 && first <= 0xdf ? 1 + : first >= 0xe0 && first <= 0xef ? 2 + : first >= 0xf0 && first <= 0xf4 ? 3 + : 0; + require(remaining != 0 && i + remaining <= text.size(), "path.encoding", + "Invalid UTF-8 path"); + const unsigned minimum = remaining == 1 ? 0x80 : remaining == 2 ? 0x800 : 0x10000; + unsigned code = first & ((1u << (6 - remaining)) - 1u); + while (remaining--) { + const auto next = static_cast(text[i++]); + require((next & 0xc0) == 0x80, "path.encoding", "Invalid UTF-8 path"); + code = (code << 6) | (next & 0x3f); + } + require(code >= minimum && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff), + "path.encoding", "Invalid UTF-8 path"); + } +} +std::string utf8_bytes(const std::u8string& value) { + return {reinterpret_cast(value.data()), value.size()}; +} +} // namespace +std::filesystem::path path_from_utf8(std::string_view text) { + validate_utf8_path(text); + return std::filesystem::path(std::u8string(text.begin(), text.end())); +} +std::string path_to_utf8(const std::filesystem::path& path) { + return utf8_bytes(path.u8string()); +} +std::string generic_path_to_utf8(const std::filesystem::path& path) { + return utf8_bytes(path.generic_u8string()); +} +std::filesystem::path native_io_path(const std::filesystem::path& path) { +#ifdef _WIN32 + auto normalized = std::filesystem::absolute(path).lexically_normal(); + normalized.make_preferred(); + const auto& native = normalized.native(); + if (native.starts_with(LR"(\\?\)") || native.starts_with(LR"(\\.\)")) + return normalized; + if (native.starts_with(LR"(\\)")) + return std::filesystem::path(std::wstring(LR"(\\?\UNC\)") + native.substr(2)); + return std::filesystem::path(std::wstring(LR"(\\?\)") + native); +#else + return path; +#endif +} +#ifdef _WIN32 +int run_utf8_main(int argc, wchar_t** argv, int (*entry)(int, char**)) noexcept { + try { + std::vector arguments; + arguments.reserve(static_cast(argc)); + for (int i = 0; i < argc; ++i) { + const auto length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv[i], -1, + nullptr, 0, nullptr, nullptr); + require(length > 0, "cli.encoding", "Invalid Unicode command-line argument"); + std::string value(static_cast(length), '\0'); + require(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv[i], -1, value.data(), + length, nullptr, nullptr) == length, + "cli.encoding", "Cannot convert command-line argument to UTF-8"); + value.pop_back(); + arguments.push_back(std::move(value)); + } + std::vector pointers; + pointers.reserve(arguments.size() + 1); + for (auto& value : arguments) + pointers.push_back(value.data()); + pointers.push_back(nullptr); + return entry(argc, pointers.data()); + } catch (const std::exception& error) { + std::cerr << "Command line failed: " << error.what() << '\n'; + return 1; + } +} +#endif std::string new_id() { static std::mutex mutex; static std::random_device random; @@ -36,27 +118,29 @@ std::string new_id() { return result; } std::string read_text(const std::filesystem::path& path) { - std::ifstream stream(path, std::ios::binary); - require(bool(stream), "io.open", "Cannot open file: " + path.string()); + std::ifstream stream(native_io_path(path), std::ios::binary); + require(bool(stream), "io.open", "Cannot open file: " + path_to_utf8(path)); std::string value((std::istreambuf_iterator(stream)), {}); - require(!stream.bad(), "io.read", "Cannot read file: " + path.string()); + require(!stream.bad(), "io.read", "Cannot read file: " + path_to_utf8(path)); return value; } Json read_json(const std::filesystem::path& path) { try { return Json::parse(read_text(path)); } catch (const Json::exception& error) { - throw Error("format.json", "Invalid JSON in " + path.string(), {{"reason", error.what()}}); + throw Error("format.json", "Invalid JSON in " + path_to_utf8(path), + {{"reason", error.what()}}); } } void atomic_write(const std::filesystem::path& path, std::string_view bytes) { const auto parent = path.has_parent_path() ? path.parent_path() : std::filesystem::path("."); - std::filesystem::create_directories(parent); - const auto temporary = parent / (path.filename().string() + ".tmp-" + new_id()); + std::filesystem::create_directories(native_io_path(parent)); + auto temporary = path; + temporary += ".tmp-" + new_id(); // Append ASCII to the native path without a narrow round trip. try { #ifdef _WIN32 - HANDLE file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, - FILE_ATTRIBUTE_NORMAL, nullptr); + HANDLE file = CreateFileW(native_io_path(temporary).c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); require(file != INVALID_HANDLE_VALUE, "io.create", "Cannot create temporary file"); bool ok = true; std::size_t offset = 0; @@ -73,9 +157,9 @@ void atomic_write(const std::filesystem::path& path, std::string_view bytes) { ok = FlushFileBuffers(file) && ok; CloseHandle(file); require(ok, "io.write", "Cannot flush temporary file"); - require(MoveFileExW(temporary.c_str(), path.c_str(), + require(MoveFileExW(native_io_path(temporary).c_str(), native_io_path(path).c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0, - "io.replace", "Cannot publish file: " + path.string()); + "io.replace", "Cannot publish file: " + path_to_utf8(path)); #else const int fd = ::open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644); require(fd >= 0, "io.create", "Cannot create temporary file"); @@ -104,7 +188,7 @@ void atomic_write(const std::filesystem::path& path, std::string_view bytes) { #endif } catch (...) { std::error_code ignored; - std::filesystem::remove(temporary, ignored); + std::filesystem::remove(native_io_path(temporary), ignored); throw; } } diff --git a/src/core/process.cpp b/src/core/process.cpp index a02cd0c..f459c6e 100644 --- a/src/core/process.cpp +++ b/src/core/process.cpp @@ -131,6 +131,41 @@ struct Process::Impl { ::close(output); #endif } +#ifndef _WIN32 + bool collect_exit() { + // WNOWAIT keeps the exited leader's PID reserved until its owned group has + // been stopped. Never signal a PGID after reaping the leader: it may be reused. + siginfo_t information{}; + int result; + do { + result = + ::waitid(P_PID, static_cast(pid), &information, WEXITED | WNOHANG | WNOWAIT); + } while (result < 0 && errno == EINTR); + if (result < 0) { + if (errno == ECHILD) { + running = false; + pid = -1; + } + throw std::runtime_error("Cannot collect child process"); + } + if (information.si_pid == 0) + return false; + ::kill(-pid, SIGKILL); + int status{}; + pid_t reaped; + do { + reaped = ::waitpid(pid, &status, 0); + } while (reaped < 0 && errno == EINTR); + running = false; + pid = -1; + if (reaped < 0) + throw std::runtime_error("Cannot reap child process"); + exit_code = WIFEXITED(status) ? WEXITSTATUS(status) + : WIFSIGNALED(status) ? 128 + WTERMSIG(status) + : 1; + return true; + } +#endif ProcessPoll poll() { std::string text; char buffer[8192]; @@ -171,20 +206,8 @@ struct Process::Impl { break; } } - if (running) { - int status{}; - pid_t result; - do { - result = ::waitpid(pid, &status, WNOHANG); - } while (result < 0 && errno == EINTR); - if (result == pid) { - running = false; - exit_code = WIFEXITED(status) ? WEXITSTATUS(status) - : WIFSIGNALED(status) ? 128 + WTERMSIG(status) - : 1; - } else if (result < 0) - throw std::runtime_error("Cannot collect child process"); - } + if (running) + collect_exit(); #endif if (!running) { #ifdef _WIN32 @@ -234,18 +257,8 @@ struct Process::Impl { ::kill(-pid, SIGTERM); const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); while (std::chrono::steady_clock::now() < deadline) { - int status{}; - auto result = ::waitpid(pid, &status, WNOHANG); - if (result == pid) { - running = false; - exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); - ::kill(-pid, SIGKILL); + if (collect_exit()) return; - } - if (result < 0 && errno != EINTR) { - running = false; - return; - } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } ::kill(-pid, SIGKILL); @@ -253,6 +266,7 @@ struct Process::Impl { while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) { } running = false; + pid = -1; exit_code = 130; #endif } diff --git a/src/editor/build_service.cpp b/src/editor/build_service.cpp index 426e25c..0b6d180 100644 --- a/src/editor/build_service.cpp +++ b/src/editor/build_service.cpp @@ -74,7 +74,7 @@ std::set asset_references(const Json& scene) { void copy_required_file(const fs::path& source, const fs::path& target) { if (!fs::is_regular_file(source) || fs::is_symlink(source)) throw std::runtime_error("Required package file missing or not a regular file: " + - source.string()); + path_to_utf8(source)); fs::create_directories(target.parent_path()); fs::copy_file(source, target, fs::copy_options::overwrite_existing); } @@ -248,9 +248,9 @@ struct BuildService::Impl { fs::create_directories(native_directory); std::vector arguments = {config.cmake, "-S", - config.engine_root.string(), + path_to_utf8(config.engine_root), "-B", - native_directory.string(), + path_to_utf8(native_directory), "-G", config.generator, "-DCMAKE_BUILD_TYPE=" + configuration, @@ -260,7 +260,7 @@ struct BuildService::Impl { "-DFASET_BUILD_RUNTIME=ON", "-DFASET_BUILD_ASSETS=ON", "-DFASET_GAMEPLAY_SOURCE_DIR=" + - (config.project_root / "Scripts").string()}; + path_to_utf8(config.project_root / "Scripts")}; bool compiler_overridden = false; for (const auto& arg : config.configure_arguments) if (arg.starts_with("-DCMAKE_CXX_COMPILER=")) @@ -268,11 +268,11 @@ struct BuildService::Impl { if (!compiler_overridden) { #ifdef _WIN32 auto compiler = find_executable("clang-cl"); - arguments.push_back("-DCMAKE_C_COMPILER=" + compiler.string()); - arguments.push_back("-DCMAKE_CXX_COMPILER=" + compiler.string()); + arguments.push_back("-DCMAKE_C_COMPILER=" + path_to_utf8(compiler)); + arguments.push_back("-DCMAKE_CXX_COMPILER=" + path_to_utf8(compiler)); #else - arguments.push_back("-DCMAKE_C_COMPILER=" + find_executable("clang").string()); - arguments.push_back("-DCMAKE_CXX_COMPILER=" + find_executable("clang++").string()); + arguments.push_back("-DCMAKE_C_COMPILER=" + path_to_utf8(find_executable("clang"))); + arguments.push_back("-DCMAKE_CXX_COMPILER=" + path_to_utf8(find_executable("clang++"))); #endif } arguments.insert(arguments.end(), config.configure_arguments.begin(), @@ -281,7 +281,7 @@ struct BuildService::Impl { run(job, std::move(arguments), config.project_root); checkpoint(job, "Compiling and linking Player", .25); run(job, - {config.cmake, "--build", native_directory.string(), "--config", configuration, + {config.cmake, "--build", path_to_utf8(native_directory), "--config", configuration, "--parallel", "4", "--target", "faset_player", "faset_schema_exporter"}, config.project_root); checkpoint(job, "Exporting gameplay schema", .58); @@ -292,7 +292,8 @@ struct BuildService::Impl { fs::create_directories(staging); try { const auto schema_file = staging / "schema.json"; - run(job, {exporter.string(), "--output", schema_file.string()}, config.project_root); + run(job, {path_to_utf8(exporter), "--output", path_to_utf8(schema_file)}, + config.project_root); auto schema = read_json(schema_file); if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 || !schema.contains("types") || !schema.at("types").is_array()) @@ -324,11 +325,11 @@ struct BuildService::Impl { atomic_write_json(config.cache_root / "last_build.json", {{"generation", job.status.id}, {"fingerprint", fingerprint}}); return {{"generation", job.status.id}, - {"directory", generation.string()}, - {"build_directory", native_directory.string()}, + {"directory", path_to_utf8(generation)}, + {"build_directory", path_to_utf8(native_directory)}, {"configuration", configuration}, - {"player", (generation / ("faset_player" + executable_suffix())).string()}, - {"schema", (generation / "schema.json").string()}, + {"player", path_to_utf8(generation / ("faset_player" + executable_suffix()))}, + {"schema", path_to_utf8(generation / "schema.json")}, {"fingerprint", fingerprint}}; } catch (...) { std::error_code error; @@ -346,7 +347,7 @@ struct BuildService::Impl { {executable.parent_path(), native_directory, native_directory / configuration}) if (fs::is_directory(root)) for (const auto& entry : fs::directory_iterator(root)) { - auto extension = entry.path().extension().string(); + auto extension = path_to_utf8(entry.path().extension()); for (auto& c : extension) c = static_cast(std::tolower(static_cast(c))); if (entry.is_regular_file() && extension == ".dll") @@ -356,7 +357,8 @@ struct BuildService::Impl { (void)job; #else const auto output = - run(job, {find_executable("ldd").string(), executable.string()}, config.project_root); + run(job, {path_to_utf8(find_executable("ldd")), path_to_utf8(executable)}, + config.project_root); if (output.find("not found") != std::string::npos) throw std::runtime_error("Player has unresolved shared library dependencies"); // SDL/physics/gameplay are linked statically. glibc/libstdc++/Vulkan are the host baseline. @@ -427,8 +429,8 @@ struct BuildService::Impl { fs::rename(staging, directory); atomic_write_json(config.cache_root / "last_cook.json", {{"generation", digest}}); return {{"generation", digest}, - {"scene", (directory / "scene.fscene").string()}, - {"directory", directory.string()}}; + {"scene", path_to_utf8(directory / "scene.fscene")}, + {"directory", path_to_utf8(directory)}}; } catch (...) { std::error_code error; fs::remove_all(staging, error); @@ -451,13 +453,14 @@ struct BuildService::Impl { for (const auto& entry : fs::directory_iterator(source)) { if (!entry.is_regular_file()) continue; - auto filename = entry.path().filename().string(); + auto filename = path_to_utf8(entry.path().filename()); std::string upper = filename; for (auto& c : upper) c = static_cast(std::toupper(static_cast(c))); if (upper.starts_with("LICENSE") || upper.starts_with("COPYING") || upper.starts_with("NOTICE")) { - copy_required_file(entry.path(), destination / name / filename); + copy_required_file(entry.path(), + destination / name / entry.path().filename()); copied = true; } } @@ -487,7 +490,7 @@ struct BuildService::Impl { "generations" / generation); auto target = destination / "assets" / id / "generations" / generation; for (const auto& file : manifest.at("files")) { - auto relative = fs::path(file.at("path").get()); + auto relative = path_from_utf8(file.at("path").get()); copy_required_file(project_path(source_directory, relative), project_path(target, relative)); } @@ -508,7 +511,8 @@ struct BuildService::Impl { for (const auto& id : asset_references(job.scene)) job.asset_manifests[id] = source.current_manifest(id); const auto built = build(job, true); - validate_component_types(job.scene, read_json(built.at("schema").get())); + validate_component_types(job.scene, + read_json(path_from_utf8(built.at("schema").get()))); auto output = fs::absolute(job.output); if (output.empty()) throw std::runtime_error("An export destination is required"); @@ -519,7 +523,7 @@ struct BuildService::Impl { try { checkpoint(job, "Cooking export snapshot", .72); write_cooked_scene(staging / "scene.fscene", job.scene); - auto build_directory = fs::path(built.at("directory").get()); + auto build_directory = path_from_utf8(built.at("directory").get()); copy_required_file(build_directory / ("faset_player" + executable_suffix()), staging / ("faset_player" + executable_suffix())); for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv", @@ -528,7 +532,7 @@ struct BuildService::Impl { copy_required_file(build_directory / "shaders" / shader, staging / "shaders" / shader); for (const auto& entry : fs::directory_iterator(build_directory)) { - auto extension = entry.path().extension().string(); + auto extension = path_to_utf8(entry.path().extension()); for (auto& c : extension) c = static_cast(std::tolower(static_cast(c))); if (entry.is_regular_file() && extension == ".dll") @@ -536,7 +540,8 @@ struct BuildService::Impl { } checkpoint(job, "Packaging assets and notices", .80); package_assets(job, staging); - package_notices(staging / "Notices", built.at("build_directory").get()); + package_notices(staging / "Notices", + path_from_utf8(built.at("build_directory").get())); atomic_write( staging / "README.txt", "Run faset_player" + executable_suffix() + @@ -553,8 +558,9 @@ struct BuildService::Impl { checkpoint(job, "Validating packaged Player", .90); auto validation_output = run(job, - {(staging / ("faset_player" + executable_suffix())).string(), "--validate", - "--scene", (staging / "scene.fscene").string(), "--assets", staging.string()}, + {path_to_utf8(staging / ("faset_player" + executable_suffix())), "--validate", + "--scene", path_to_utf8(staging / "scene.fscene"), "--assets", + path_to_utf8(staging)}, staging); Json files = Json::array(); for (const auto& entry : fs::recursive_directory_iterator(staging)) { @@ -562,7 +568,7 @@ struct BuildService::Impl { throw std::runtime_error("Export contains a symlink"); if (entry.is_regular_file()) files.push_back( - {{"path", entry.path().lexically_relative(staging).generic_string()}, + {{"path", generic_path_to_utf8(entry.path().lexically_relative(staging))}, {"sha256", sha256_file(entry.path())}, {"size", entry.file_size()}}); } @@ -613,15 +619,16 @@ struct BuildService::Impl { {"version", 1}, {"generation", job.status.id}, {"directory", "generations/" + job.status.id}}); - return {{"directory", generation.string()}, - {"executable", (generation / ("faset_player" + executable_suffix())).string()}, - {"manifest", (generation / "manifest.json").string()}, - {"generation", job.status.id}, - {"build", built}, - {"schema", built.at("schema")}, - {"player", built.at("player")}, - {"build_directory", built.at("build_directory")}, - {"configuration", built.at("configuration")}}; + return { + {"directory", path_to_utf8(generation)}, + {"executable", path_to_utf8(generation / ("faset_player" + executable_suffix()))}, + {"manifest", path_to_utf8(generation / "manifest.json")}, + {"generation", job.status.id}, + {"build", built}, + {"schema", built.at("schema")}, + {"player", built.at("player")}, + {"build_directory", built.at("build_directory")}, + {"configuration", built.at("configuration")}}; } catch (...) { std::error_code error; fs::remove_all(staging, error); diff --git a/src/editor/commands.cpp b/src/editor/commands.cpp index 9086bc8..57e14bb 100644 --- a/src/editor/commands.cpp +++ b/src/editor/commands.cpp @@ -68,9 +68,9 @@ Json Commands::call(const std::string& name, const Json& arguments) { Json Commands::resolved_scene(const std::string& id) const { const auto result = authoring::resolve_templates( authoring_.query(id).at("scene"), authoring_.schemas(), [&](const std::string& path) { - const auto relative = std::filesystem::path(path).lexically_normal(); + const auto relative = path_from_utf8(path).lexically_normal(); for (const auto& document : authoring_.documents()) - if (document.at("path") == relative.generic_string()) + if (document.at("path") == generic_path_to_utf8(relative)) return authoring_.query(document.at("id")).at("scene"); return read_json(project_path(authoring_.root(), relative)); }); @@ -92,7 +92,7 @@ Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authorin "Open a scene relative to the project. Set recover=true to load its saved recovery " "journal.", object_schema({{"path", text}, {"recover", boolean}}, {"path"}), [&](const Json& args) { - return authoring_.open(args.at("path").get(), + return authoring_.open(path_from_utf8(args.at("path").get()), args.value("recover", false)); }); add( @@ -104,7 +104,8 @@ Commands::Commands(authoring::AuthoringService& authoring) : authoring_(authorin add("faset_document_save", "Atomically save an authoring document. Refuses to overwrite an externally modified file.", object_schema({{"document", text}, {"path", text}}, {"document"}), [&](const Json& args) { - return authoring_.save(args.at("document"), args.value("path", std::string())); + return authoring_.save(args.at("document"), + path_from_utf8(args.value("path", std::string()))); }); add( "faset_schema", diff --git a/src/editor/editor_ui.cpp b/src/editor/editor_ui.cpp index 2eb0488..8d18a89 100644 --- a/src/editor/editor_ui.cpp +++ b/src/editor/editor_ui.cpp @@ -174,6 +174,7 @@ struct EditorUI::Impl { std::uint64_t shown_revision = std::numeric_limits::max(); std::map edit_revisions; std::map preview_fields; + std::map import_retries; std::filesystem::path layout_path; std::filesystem::path theme_source_path, layout_source_path; std::string attempted_theme, attempted_layout, applied_theme, applied_layout, @@ -196,6 +197,14 @@ struct EditorUI::Impl { std::uint64_t gizmo_revision = 0; std::string gizmo_component; ui::Rect viewport; + float ui_scale = 1; + unsigned layout_width = 0, layout_height = 0; + float logical_width() const { + return float(renderer.width()) / ui_scale; + } + float logical_height() const { + return float(renderer.height()) / ui_scale; + } Vec3 gizmo_origin{}; render::Vec2 gizmo_drag_start{}, gizmo_drag_end{}; float gizmo_world_length = 1; @@ -253,12 +262,13 @@ struct EditorUI::Impl { document = session.authoring() .create("Untitled", session.project().value("dimension", 3)) .at("id"); + ui_scale = std::clamp(renderer.display_scale(), .5f, 4.f); refresh(); } void persist_layout() { - dock.set_size("scene", ui.find("scene_panel")->rect.width); - dock.set_size("inspector", ui.find("inspector_panel")->rect.width); - dock.set_size("bottom", ui.find("bottom_panel")->rect.height); + dock.set_size("scene", ui.find("scene_panel")->rect.width / ui_scale); + dock.set_size("inspector", ui.find("inspector_panel")->rect.width / ui_scale); + dock.set_size("bottom", ui.find("bottom_panel")->rect.height / ui_scale); try { dock.save(layout_path); } catch (const std::exception& e) { @@ -897,7 +907,8 @@ struct EditorUI::Impl { } if (!open) try { - source_scene = read_json(project_path(session.config().project_root, source)); + source_scene = read_json( + project_path(session.config().project_root, path_from_utf8(source))); } catch (...) { return source; } @@ -983,7 +994,8 @@ struct EditorUI::Impl { } void instance_scene(const std::string& path) { try { - auto data = read_json(project_path(session.config().project_root, path)); + auto data = + read_json(project_path(session.config().project_root, path_from_utf8(path))); authoring::validate_scene(data, session.authoring().schemas()); if (data.at("id") == document) throw std::runtime_error("A scene cannot instance itself"); @@ -1019,7 +1031,7 @@ struct EditorUI::Impl { subtree.insert(item.at("id").get()).second || changed; } auto result = call("faset_document_create", - {{"name", std::filesystem::path(path).stem().string()}, + {{"name", path_to_utf8(path_from_utf8(path).stem())}, {"dimension", current.at("scene").value("dimension", 3)}}); if (result.is_null()) return; @@ -1075,9 +1087,9 @@ struct EditorUI::Impl { const bool image = manifest.is_object() && manifest.value("kind", std::string()) == "image"; const auto name = manifest.is_object() - ? std::filesystem::path(manifest.value("source", std::string("Imported asset"))) - .stem() - .string() + ? path_to_utf8( + path_from_utf8(manifest.value("source", std::string("Imported asset"))) + .stem()) : "Imported asset"; auto object = authoring::make_entity(session.authoring().schemas(), name); const auto entity_id = object.at("id").get(); @@ -1218,7 +1230,7 @@ struct EditorUI::Impl { std::function append_group = [&](Json path, int depth) { const auto source = instance_source(path); auto& row = tree.add(Kind::TreeRow, "instance-" + path.dump(), - "[T] " + std::filesystem::path(source).filename().string()); + "[T] " + path_to_utf8(path_from_utf8(source).filename())); row.indent = depth; row.selected = instance_selection == path; row.on_click = [this, path](Widget&) { select_instance(path); }; @@ -1378,7 +1390,7 @@ struct EditorUI::Impl { const auto path = e->at("origin").at("path"); const auto object = e->at("origin").at("object").get(); label(body, "object-origin", - "Source: " + std::filesystem::path(instance_source(path)).filename().string()); + "Source: " + path_to_utf8(path_from_utf8(instance_source(path)).filename())); keep.insert("object-origin"); button(body, "object-open-source", "Open source", [this, path, object] { open_template_source(path, object); }); @@ -1565,9 +1577,8 @@ struct EditorUI::Impl { break; if (!entry.is_regular_file()) continue; - auto relative = - std::filesystem::relative(entry.path(), session.config().project_root) - .generic_string(); + auto relative = generic_path_to_utf8( + std::filesystem::relative(entry.path(), session.config().project_root)); if (relative.find(".faset-") != std::string::npos) continue; if (!asset_filter.empty() && relative.find(asset_filter) == std::string::npos) @@ -1602,11 +1613,10 @@ struct EditorUI::Impl { } for (const auto& asset : assets) { const auto id = asset.at("id").get(); - const auto name = asset.contains("manifest") - ? std::filesystem::path(asset["manifest"].value("source", id)) - .filename() - .string() - : id; + const auto name = + asset.contains("manifest") + ? path_to_utf8(path_from_utf8(asset["manifest"].value("source", id)).filename()) + : id; auto& row = list.add(Kind::TreeRow, "asset-" + id, "Imported / " + name); row.layout.height = 25; row.indent = 1; @@ -1624,6 +1634,8 @@ struct EditorUI::Impl { trim_children(list, keep); } void refresh_bottom() { + const auto jobs_response = call("faset_jobs"); + const auto all_jobs = jobs_response.is_null() ? Json::array() : jobs_response.at("jobs"); for (const auto& panel : session.plugin_panels()) { const auto panel_id = panel.at("id").get(); const auto dock_id = "plugin-" + panel_id; @@ -1674,8 +1686,6 @@ struct EditorUI::Impl { ui.find("console-items")->visible = active_bottom == "console"; ui.find("job-items")->visible = active_bottom == "jobs"; ui.find("conflict-items")->visible = active_bottom == "conflicts"; - ui.find("tab-conflicts")->text = - "Conflicts (" + std::to_string(template_conflicts.size()) + ")"; auto& conflict_list = *ui.find("conflict-items"); std::set conflict_keep; std::size_t conflict_index = 0; @@ -1687,7 +1697,7 @@ struct EditorUI::Impl { const auto path = conflict.at("instance_path"); auto& text = row.add( Kind::Label, id + "-text", - code + " · " + std::filesystem::path(instance_source(path)).filename().string()); + code + " · " + path_to_utf8(path_from_utf8(instance_source(path)).filename())); text.layout.flex = 1; button( row, id + "-source", "Open source", [this, path] { open_template_source(path); }, @@ -1716,9 +1726,113 @@ struct EditorUI::Impl { } conflict_keep.insert(id); } + std::size_t import_conflicts = 0; + for (const auto& job : all_jobs) { + if (job.value("kind", std::string()) != "import" || + job.value("state", std::string()) != "conflict") + continue; + const auto job_id = job.at("id").get(); + const auto& result = job.at("result"); + const auto request = job.value("request", Json::object()); + bool pending = false, superseded = false; + std::string retry_error; + if (const auto found = import_retries.find(job_id); found != import_retries.end()) + for (const auto& retry : all_jobs) + if (retry.at("id") == found->second) { + const auto state = retry.value("state", std::string()); + pending = state == "queued" || state == "running"; + superseded = state == "succeeded" || state == "conflict"; + if (state == "failed" || state == "cancelled") + retry_error = retry.value("error", std::string("Retry cancelled")); + } + for (const auto& asset : assets) + if (asset.at("id") == result.value("asset_id", std::string()) && + asset.contains("manifest") && + asset.at("manifest").at("generation") == + result.value("generation", std::string())) + superseded = true; + if (superseded) + continue; + ++import_conflicts; + const auto id = "import-conflict-" + job_id; + auto& group = conflict_list.add(Kind::Column, id); + group.layout.padding = 6; + group.layout.gap = 3; + conflict_keep.insert(id); + label(group, id + "-title", "Import removal: " + request.value("path", job_id)); + label(group, id + "-note", + "The previous asset stays active. Update affected references before accepting."); + label(group, id + "-generation", + "Reviewed generation: " + result.value("generation", std::string())); + auto diagnostic = job.value("error", std::string()); + std::replace(diagnostic.begin(), diagnostic.end(), '\n', ' '); + label(group, id + "-diagnostic", diagnostic); + const auto removed = result.value("removed_output_ids", Json::array()); + auto& outputs = group.add(Kind::Column, id + "-outputs"); + outputs.layout.height = std::min(150.f, std::max(26.f, float(removed.size()) * 26)); + outputs.layout.scroll = true; + outputs.layout.gap = 0; + std::set output_keep; + for (const auto& output : removed) { + const auto output_id = output.get(); + std::string name; + for (const auto& asset : assets) + if (asset.at("id") == result.value("asset_id", std::string()) && + asset.contains("manifest")) + for (const auto* collection : {"nodes", "meshes", "materials", "textures"}) + for (const auto& item : + asset.at("manifest").value(collection, Json::array())) + if (item.at("id") == output_id) + name = item.value("name", std::string()); + const auto output_widget = id + "-output-" + output_id; + label(outputs, output_widget, output_id + (name.empty() ? "" : " / " + name)); + outputs.find(output_widget)->layout.height = 26; + output_keep.insert(output_widget); + } + trim_children(outputs, output_keep); + auto& actions = group.add(Kind::Row, id + "-actions"); + actions.layout.height = 30; + const auto retry = [this, job_id, request, result](bool accept) { + Json arguments = {{"path", request.at("path")}, {"allow_removed_outputs", accept}}; + if (request.contains("settings") && !request.at("settings").is_null()) + arguments["settings"] = request.at("settings"); + if (accept) { + arguments["expected_generation"] = result.at("generation"); + arguments["expected_active_generation"] = result.at("previous_generation"); + } + auto submitted = call("faset_import", arguments); + if (!submitted.is_null()) { + import_retries[job_id] = submitted.at("job"); + status = + accept + ? "Publishing the reviewed import; source changes require a new review" + : "Reimporting source for a fresh review"; + } + }; + button( + actions, id + "-retry", "Reimport / review again", [retry] { retry(false); }, 185) + .enabled = !pending && request.contains("path"); + button( + actions, id + "-accept", "Accept reviewed removal", [retry] { retry(true); }, 190) + .enabled = !pending && request.contains("path") && !removed.empty(); + button( + actions, id + "-copy", "Copy removed IDs", + [this, removed] { renderer.set_clipboard(removed.dump(2)); }, 145); + auto detail = pending ? "Import in progress; the previous generation remains active." + : "Accepting removes these outputs. Scene references are not " + "remapped automatically."; + label(group, id + "-detail", detail); + if (!retry_error.empty()) { + std::replace(retry_error.begin(), retry_error.end(), '\n', ' '); + label(group, id + "-error", retry_error); + } else + group.remove(id + "-error"); + } + ui.find("tab-conflicts")->text = + "Conflicts (" + std::to_string(template_conflicts.size() + import_conflicts) + ")"; if (conflict_keep.empty()) { label(conflict_list, "conflicts-empty", - "No template conflicts. Missing targets preserve their overrides here."); + "No template or import conflicts. Missing targets preserve their data here."); conflict_keep.insert("conflicts-empty"); } trim_children(conflict_list, conflict_keep); @@ -1738,27 +1852,28 @@ struct EditorUI::Impl { keep.insert("log-empty"); } trim_children(console, keep); - auto result = call("faset_jobs"); auto& jobs = *ui.find("job-items"); keep.clear(); - if (!result.is_null()) - for (const auto& job : result.at("jobs")) { - const auto id = job.at("id").get(); - auto& row = jobs.add(Kind::Row, "job-" + id); - row.layout.height = 28; - keep.insert(row.id); - const auto state = job.value("state", std::string()); - auto& text = - row.add(Kind::Label, "job-text-" + id, - job.value("kind", std::string("Job")) + " · " + state + " · " + - job.value("stage", std::string()) + " " + - std::to_string(int(job.value("progress", 0.0) * 100)) + "%"); - text.layout.flex = 1; - auto& cancel = button( - row, "job-cancel-" + id, "Cancel", - [this, id] { call("faset_job_cancel", {{"id", id}}); }, 72); - cancel.enabled = state == "queued" || state == "running"; - } + for (const auto& job : all_jobs) { + const auto id = job.at("id").get(); + auto& row = jobs.add(Kind::Row, "job-" + id); + row.layout.height = 28; + keep.insert(row.id); + const auto state = job.value("state", std::string()); + auto& text = row.add(Kind::Label, "job-text-" + id, + job.value("kind", std::string("Job")) + " · " + state + " · " + + job.value("stage", std::string()) + " " + + std::to_string(int(job.value("progress", 0.0) * 100)) + "%"); + text.layout.flex = 1; + auto& cancel = button( + row, "job-cancel-" + id, "Cancel", + [this, id] { call("faset_job_cancel", {{"id", id}}); }, 72); + cancel.enabled = state == "queued" || state == "running"; + auto& review = button( + row, "job-review-" + id, "Review removals", [this] { active_bottom = "conflicts"; }, + 135); + review.visible = job.value("kind", std::string()) == "import" && state == "conflict"; + } if (keep.empty()) { label(jobs, "jobs-empty", "No active import, build or export jobs."); keep.insert("jobs-empty"); @@ -1788,8 +1903,8 @@ struct EditorUI::Impl { : "Project switching is unavailable while MCP is connected"; auto* switching = ui.find("project-switch-dialog"); switching->visible = project_switch_warning; - switching->layout.x = std::max(0.f, (float(renderer.width()) - 500) * .5f); - switching->layout.y = std::max(0.f, (float(renderer.height()) - 220) * .5f); + switching->layout.x = std::max(0.f, (logical_width() - 500) * .5f); + switching->layout.y = std::max(0.f, (logical_height() - 220) * .5f); for (const auto* id : {"menu-duplicate", "menu-delete"}) ui.find(id)->visible = edit; for (const auto* id : {"help-one", "help-two", "help-three"}) @@ -1805,7 +1920,7 @@ struct EditorUI::Impl { : 5; auto& command = *ui.find("palette"); command.visible = palette; - command.layout.x = std::max(0.f, (float(renderer.width()) - 570) / 2); + command.layout.x = std::max(0.f, (logical_width() - 570) / 2); command.layout.y = 90; auto& list = *ui.find("palette-list"); std::set keep; @@ -1855,10 +1970,9 @@ struct EditorUI::Impl { if (++visited > 4000) break; if (entry.is_regular_file() && - entry.path().filename().string().ends_with(".scene.json")) - saved.insert( - std::filesystem::relative(entry.path(), session.config().project_root) - .generic_string()); + path_to_utf8(entry.path().filename()).ends_with(".scene.json")) + saved.insert(generic_path_to_utf8(std::filesystem::relative( + entry.path(), session.config().project_root))); } } } catch (const std::exception& error) { @@ -1867,7 +1981,8 @@ struct EditorUI::Impl { std::size_t index = 0; for (const auto& path : saved) { try { - const auto scene = read_json(project_path(session.config().project_root, path)); + const auto scene = + read_json(project_path(session.config().project_root, path_from_utf8(path))); if (scene.value("format", "") != "faset.scene" || scene.value("version", 0) != 1) continue; } catch (...) { @@ -1914,10 +2029,10 @@ struct EditorUI::Impl { void refresh_project_settings() { auto* panel = ui.find("project-settings-panel"); panel->visible = project_settings_open; - panel->layout.width = std::min(610.f, std::max(340.f, float(renderer.width()) - 40)); - panel->layout.height = std::min(550.f, std::max(300.f, float(renderer.height()) - 40)); - panel->layout.x = std::max(0.f, (float(renderer.width()) - panel->layout.width) * .5f); - panel->layout.y = std::max(0.f, (float(renderer.height()) - panel->layout.height) * .5f); + panel->layout.width = std::min(610.f, std::max(340.f, logical_width() - 40)); + panel->layout.height = std::min(550.f, std::max(300.f, logical_height() - 40)); + panel->layout.x = std::max(0.f, (logical_width() - panel->layout.width) * .5f); + panel->layout.y = std::max(0.f, (logical_height() - panel->layout.height) * .5f); ui.find("project-settings-2d")->selected = project_settings_dimension == 2; ui.find("project-settings-3d")->selected = project_settings_dimension == 3; ui.find("project-settings-error")->text = project_settings_error; @@ -1928,7 +2043,7 @@ struct EditorUI::Impl { void refresh_simulation() { auto& panel = *ui.find("simulation-panel"); panel.visible = simulation_open; - panel.layout.x = std::max(0.f, (float(renderer.width()) - 470) / 2); + panel.layout.x = std::max(0.f, (logical_width() - 470) / 2); panel.layout.y = 90; if (!simulation_open) return; @@ -1986,7 +2101,7 @@ struct EditorUI::Impl { void refresh_recovery() { auto& panel = *ui.find("recovery-panel"); panel.visible = !recovery.empty(); - panel.layout.x = std::max(0.f, (float(renderer.width()) - 470) / 2); + panel.layout.x = std::max(0.f, (logical_width() - 470) / 2); panel.layout.y = 100; std::set keep; label(panel, "recovery-title", "Unsaved authoring recovery"); @@ -2152,11 +2267,16 @@ struct EditorUI::Impl { const render::Color colors[3] = { {.88f, .35f, .34f, 1}, {.38f, .75f, .49f, 1}, {.4f, .58f, .92f, 1}}; for (int axis = 0; axis < (is2d ? 2 : 3); ++axis) { - line(gizmo_screen[0], gizmo_screen[axis + 1], colors[axis], 2); + line(gizmo_screen[0], gizmo_screen[axis + 1], colors[axis], 2 * ui_scale); const auto end = gizmo_screen[axis + 1]; if (viewport.contains(end[0], end[1])) - rendered.ui_quads.push_back( - {end[0] - 4, end[1] - 4, 8, 8, colors[axis], {}, {0, 0, 1, 1}}); + rendered.ui_quads.push_back({end[0] - 4 * ui_scale, + end[1] - 4 * ui_scale, + 8 * ui_scale, + 8 * ui_scale, + colors[axis], + {}, + {0, 0, 1, 1}}); } } } @@ -2196,12 +2316,12 @@ struct EditorUI::Impl { const auto* c = component(*e, "faset.transform"); if (!c || c->value("version", 1) != 1) return false; - float best = 8; + float best = 8 * ui_scale; int axis = -1; for (int i = 0; i < (current.at("scene").value("dimension", 3) == 2 ? 2 : 3); ++i) { auto a = gizmo_screen[0], b = gizmo_screen[i + 1]; const auto dx = b[0] - a[0], dy = b[1] - a[1], l = dx * dx + dy * dy; - if (l < 16) + if (l < 16 * ui_scale * ui_scale) continue; const auto t = std::clamp(((x - a[0]) * dx + (y - a[1]) * dy) / l, 0.f, 1.f); const auto d = std::hypot(x - a[0] - t * dx, y - a[1] - t * dy); @@ -2282,8 +2402,8 @@ struct EditorUI::Impl { if (camera_drag) { const bool is2d = current.at("scene").value("dimension", 3) == 2; if (camera_drag == 3 && !is2d) { - yaw -= dx * .008f; - pitch = std::clamp(pitch + dy * .008f, -1.5f, 1.5f); + yaw -= dx / ui_scale * .008f; + pitch = std::clamp(pitch + dy / ui_scale * .008f, -1.5f, 1.5f); } else { const auto right = Vec3{std::cos(yaw), 0, -std::sin(yaw)}; const auto up = @@ -2367,7 +2487,7 @@ struct EditorUI::Impl { } } if (event.type == Type::MouseDown && !menu.empty() && - !ui.find("menu-popup")->rect.contains(event.x, event.y) && event.y > 36) + !ui.find("menu-popup")->rect.contains(event.x, event.y) && event.y > 36 * ui_scale) menu.clear(); if (event.type == Type::MouseMove || event.type == Type::MouseDown) { mouse_x = event.x; @@ -2427,15 +2547,31 @@ struct EditorUI::Impl { } } void frame(const std::vector& events_) { + const auto next_scale = std::clamp(renderer.display_scale(), .5f, 4.f); + const bool geometry_changed = next_scale != ui_scale || layout_width != renderer.width() || + layout_height != renderer.height(); + if (next_scale != ui_scale) { + camera_drag = 0; + if (gizmo_axis >= 0) { + const auto field = gizmo_mode == "Move" ? "position" + : gizmo_mode == "Rotate" ? "rotation" + : "scale"; + preview_fields.erase(gizmo_component + "/" + field); + gizmo_axis = -1; + } + } + ui_scale = next_scale; + layout_width = renderer.width(); + layout_height = renderer.height(); session.poll(); poll_presentation(); refresh(); - ui.layout(float(renderer.width()), float(renderer.height())); - if (rendered.scene_rect[2] == 0) + ui.layout(float(renderer.width()), float(renderer.height()), ui_scale); + if (rendered.scene_rect[2] == 0 || geometry_changed) build_snapshot(); events(events_); refresh(); - ui.layout(float(renderer.width()), float(renderer.height())); + ui.layout(float(renderer.width()), float(renderer.height()), ui_scale); build_snapshot(); } }; diff --git a/src/editor/plugins.cpp b/src/editor/plugins.cpp index b8105c6..4de453a 100644 --- a/src/editor/plugins.cpp +++ b/src/editor/plugins.cpp @@ -202,7 +202,8 @@ struct PluginManager::Impl { module->owner = this; module->manifest = manifest; module->directory = directory; - const auto path = project_path(directory, manifest.at("library").get()); + const auto path = + project_path(directory, path_from_utf8(manifest.at("library").get())); require(std::filesystem::is_regular_file(path), "plugin.library", "Plugin library is missing"); #ifdef _WIN32 @@ -307,8 +308,8 @@ void PluginManager::load(const std::filesystem::path& directory) { }; for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) if (entry.is_regular_file() && - entry.path().filename().string().ends_with(".faset-plugin.json")) { - std::string id = entry.path().filename().string(); + path_to_utf8(entry.path().filename()).ends_with(".faset-plugin.json")) { + std::string id = path_to_utf8(entry.path().filename()); try { const auto manifest = read_json(entry.path()); id = manifest.at("id"); @@ -324,7 +325,8 @@ void PluginManager::load(const std::filesystem::path& directory) { manifest.at("build_fingerprint") == fingerprint(), "plugin.compatibility", "Plugin manifest does not match this Editor SDK; rebuild it"); - project_path(entry.path().parent_path(), manifest.at("library").get()); + project_path(entry.path().parent_path(), + path_from_utf8(manifest.at("library").get())); sources.emplace(id, Source{manifest, entry.path().parent_path()}); } catch (const std::exception& error) { failure(id, error.what()); diff --git a/src/editor/session.cpp b/src/editor/session.cpp index c6a5457..71c3fe6 100644 --- a/src/editor/session.cpp +++ b/src/editor/session.cpp @@ -34,7 +34,7 @@ struct Session::ImportTask { std::shared_ptr job = std::make_shared(); mutable std::mutex mutex; std::string state = "queued", error; - Json result = Json::object(); + Json result = Json::object(), request = Json::object(); Json json() const { std::lock_guard lock(mutex); const auto progress = job->progress(); @@ -44,6 +44,7 @@ struct Session::ImportTask { {"stage", progress.stage}, {"progress", progress.fraction}, {"error", error}, + {"request", request}, {"result", result}}; } }; @@ -97,13 +98,13 @@ Json Session::project() const { "Project start_scene must be a relative path"); const auto scene = value.at("start_scene").get(); if (!scene.empty()) - project_path(config_.project_root, scene); + project_path(config_.project_root, path_from_utf8(scene)); } return value; } return {{"format", "faset.project"}, {"version", 1}, - {"name", config_.project_root.filename().string()}, + {"name", path_to_utf8(config_.project_root.filename())}, {"dimension", 3}}; } void Session::scaffold(const std::string& name, int dimension) { @@ -117,12 +118,12 @@ Json Session::assets_list() const { for (const auto& entry : std::filesystem::directory_iterator(directory)) if (entry.is_directory()) { try { - const auto id = entry.path().filename().string(); + const auto id = path_to_utf8(entry.path().filename()); const auto manifest = assets_.current_manifest(id); list.push_back({{"id", id}, {"manifest", manifest}}); } catch (const std::exception& error) { list.push_back( - {{"id", entry.path().filename().string()}, {"error", error.what()}}); + {{"id", path_to_utf8(entry.path().filename())}, {"error", error.what()}}); } } return {{"assets", list}}; @@ -137,8 +138,8 @@ std::string Session::source_signature() const { std::sort(files.begin(), files.end()); std::string contents; for (const auto& file : files) - contents += - file.lexically_relative(directory).generic_string() + ":" + sha256_file(file) + "\n"; + contents += generic_path_to_utf8(file.lexically_relative(directory)) + ":" + + sha256_file(file) + "\n"; return sha256(contents); } Json Session::schema_status() const { @@ -181,8 +182,8 @@ void Session::launch_player(Json scene, const std::filesystem::path& executable) control_sequence_ = 0; ProcessOptions options; options.arguments = { - executable.string(), "--scene", snapshot.string(), "--assets", - assets_.cache_root().string(), "--control", control_path_.string()}; + path_to_utf8(executable), "--scene", path_to_utf8(snapshot), "--assets", + path_to_utf8(assets_.cache_root()), "--control", path_to_utf8(control_path_)}; options.working_directory = config_.project_root; player_ = std::make_unique(options); log("Play started in a separate Player process"); @@ -194,10 +195,34 @@ void Session::stop_player() { pending_play_scene_ = nullptr; } if (player_) { - player_->cancel(); - const auto result = player_->poll(); - log(result.output); + // Allow the normal Player shutdown path (including gameplay OnDestroy) first. + // Keep Stop synchronous and bounded so starting a new Play cannot overlap this one. + bool finished = false; + try { + atomic_write_json(control_path_, + {{"sequence", ++control_sequence_}, {"command", "stop"}}); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); + for (;;) { + const auto result = player_->poll(); + log(result.output); + if (!result.running) { + finished = true; + break; + } + if (std::chrono::steady_clock::now() >= deadline) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + } catch (const std::exception& error) { + log(std::string("Graceful Player stop failed: ") + error.what()); + } + if (!finished) { + log("Player did not finish graceful shutdown; terminating the process"); + player_->cancel(); + log(player_->poll().output); + } player_.reset(); + control_path_.clear(); log("Play stopped; authoring scene unchanged"); } } @@ -224,7 +249,7 @@ void Session::poll() { log(value.kind + " completed"); if (value.result.contains("schema")) try { - load_schema(value.result.at("schema").get()); + load_schema(path_from_utf8(value.result.at("schema").get())); // This signature represents the sources submitted with this job, not later // edits. if (value.result.contains("source_signature")) @@ -245,10 +270,9 @@ void Session::poll() { if (value.state == "succeeded" && schema_valid) try { const auto executable = - value.result.value("player", (builds_.config().build_directory / - executable_name("faset_player")) - .string()); - launch_player(pending_play_scene_, executable); + value.result.value("player", path_to_utf8(builds_.config().build_directory / + executable_name("faset_player"))); + launch_player(pending_play_scene_, path_from_utf8(executable)); } catch (const std::exception& error) { log(std::string("Play failed: ") + error.what()); } @@ -332,7 +356,8 @@ void Session::register_commands() { else { require(field.is_string() && !field.get().empty(), "project.start_scene", "Choose a saved scene inside the project"); - const auto file = project_path(config_.project_root, field.get()); + const auto file = project_path(config_.project_root, + path_from_utf8(field.get())); require(std::filesystem::is_regular_file(file), "project.start_scene", "Save the start scene before selecting it in Project settings"); const auto scene = read_json(file); @@ -358,15 +383,25 @@ void Session::register_commands() { "a cancellable job ID; failure retains the last successful generation.", schema({{"path", text}, {"settings", {{"type", "object"}}}, - {"allow_removed_outputs", boolean}}, + {"allow_removed_outputs", boolean}, + {"expected_generation", text}, + {"expected_active_generation", text}}, {"path"}), [&](const Json& args) { assets::ImportRequest request; - request.source = project_path(config_.project_root, args.at("path").get()); + request.source = project_path(config_.project_root, + path_from_utf8(args.at("path").get())); request.settings = args.value("settings", Json(nullptr)); request.allow_removed_outputs = args.value("allow_removed_outputs", false); + request.expected_generation = args.value("expected_generation", std::string()); + request.expected_active_generation = + args.value("expected_active_generation", std::string()); auto task = std::make_shared(); task->id = "import-" + new_id(); + task->request = {{"path", generic_path_to_utf8(std::filesystem::relative( + request.source, config_.project_root))}, + {"settings", request.settings}, + {"allow_removed_outputs", request.allow_removed_outputs}}; imports_[task->id] = task; workers_.emplace_back([this, task, request] { { @@ -382,6 +417,8 @@ void Session::register_commands() { : "failed"; task->result = {{"asset_id", result.asset_id}, {"generation", result.generation}, + {"previous_generation", result.previous_generation}, + {"removed_output_ids", result.removed_output_ids}, {"diagnostics", result.diagnostics}, {"cache_hit", result.cache_hit}, {"manifest", result.manifest}}; @@ -417,7 +454,8 @@ void Session::register_commands() { const auto signature = source_signature(); const auto id = builds_.start_export( resolved_or_throw(commands_, args.at("document")), - project_path(config_.project_root, args.at("output").get())); + project_path(config_.project_root, + path_from_utf8(args.at("output").get()))); submitted_sources_[id] = signature; return Json{{"job", id}}; }); diff --git a/src/player/SceneView.cpp b/src/player/SceneView.cpp index 3d249d4..29c4a81 100644 --- a/src/player/SceneView.cpp +++ b/src/player/SceneView.cpp @@ -300,6 +300,7 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting out.light_direction = direction(model, {-0.5f, -1, -0.3f}); if (auto fields = properties(entity, "sprite"); !fields.is_null()) { render::Sprite sprite; + sprite.layer = fields.value("layer", 0); sprite.position = point(model, {0, 0, 0}); auto size = vec<2>(fields, "size", {1, 1}); float sx = std::hypot(model[0], model[1]), sy = std::hypot(model[4], model[5]); diff --git a/src/player/scene_io.cpp b/src/player/scene_io.cpp index 0e1caaa..f21b05c 100644 --- a/src/player/scene_io.cpp +++ b/src/player/scene_io.cpp @@ -6,8 +6,8 @@ namespace faset::player { nlohmann::json readScene(const std::filesystem::path& path) { if (!std::filesystem::is_regular_file(path)) - throw std::runtime_error("Scene file does not exist: " + path.string()); - if (std::filesystem::file_size(path) > 256 * 1024 * 1024) + throw std::runtime_error("Scene file does not exist: " + faset::path_to_utf8(path)); + if (std::filesystem::file_size(faset::native_io_path(path)) > 256 * 1024 * 1024) throw std::runtime_error("Scene exceeds the 256 MiB reader limit"); const auto bytes = faset::read_text(path); if (bytes.size() >= 8 && std::memcmp(bytes.data(), "FASETSCN", 8) == 0) { diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp index 9c9a0d2..1bfa522 100644 --- a/src/render/renderer.cpp +++ b/src/render/renderer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -88,7 +89,7 @@ struct Renderer::Impl { VkDescriptorPool descriptor_pool{}; VkSampler shadow_sampler{}, color_sampler{}; VkPipelineLayout pipeline_layout{}; - VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}; + VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{}; struct GpuTexture { Image image; VkDescriptorSet descriptor{}; @@ -150,6 +151,8 @@ struct Renderer::Impl { vkDestroyPipeline(device, ui_pipeline, nullptr); if (shadow_pipeline) vkDestroyPipeline(device, shadow_pipeline, nullptr); + if (sprite_pipeline) + vkDestroyPipeline(device, sprite_pipeline, nullptr); if (pipeline_layout) vkDestroyPipelineLayout(device, pipeline_layout, nullptr); if (descriptor_pool) @@ -207,7 +210,8 @@ struct Renderer::Impl { VkMemoryPropertyFlags properties, VkMemoryPropertyFlags preferred = 0) { Buffer b{}; b.size = bytes; - VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + VkBufferCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.size = bytes; info.usage = usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -215,7 +219,8 @@ struct Renderer::Impl { try { VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device, b.handle, &req); - VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type(req.memoryTypeBits, properties, preferred); check(vkAllocateMemory(device, &alloc, nullptr, &b.memory), "Allocate buffer memory"); @@ -230,7 +235,8 @@ struct Renderer::Impl { Image make_image(std::uint32_t w, std::uint32_t h, VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspect) { Image image{}; - VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + VkImageCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; info.imageType = VK_IMAGE_TYPE_2D; info.format = format; info.extent = {w, h, 1}; @@ -244,7 +250,8 @@ struct Renderer::Impl { try { VkMemoryRequirements req{}; vkGetImageMemoryRequirements(device, image.handle, &req); - VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); @@ -252,7 +259,8 @@ struct Renderer::Impl { "Allocate image memory"); image.allocation_size = req.size; check(vkBindImageMemory(device, image.handle, image.memory, 0), "Bind image memory"); - VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + VkImageViewCreateInfo view{}; + view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view.image = image.handle; view.viewType = VK_IMAGE_VIEW_TYPE_2D; view.format = format; @@ -267,7 +275,8 @@ struct Renderer::Impl { void transition(VkCommandBuffer cmd, VkImage image, VkImageLayout& before, VkImageLayout after, VkImageAspectFlags aspect) { // Conservative dependencies make the first single-queue backend auditable. - VkImageMemoryBarrier2 barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2}; + VkImageMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; barrier.srcStageMask = before == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; @@ -280,7 +289,8 @@ struct Renderer::Impl { barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; barrier.subresourceRange = {aspect, 0, 1, 0, 1}; - VkDependencyInfo dependency{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &barrier; vkCmdPipelineBarrier2(cmd, &dependency); @@ -292,20 +302,23 @@ struct Renderer::Impl { } void begin() { check(vkResetCommandBuffer(command, 0), "Reset command buffer"); - VkCommandBufferBeginInfo info{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + VkCommandBufferBeginInfo info{}; + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; check(vkBeginCommandBuffer(command, &info), "Begin command buffer"); } void submit(bool present = false) { check(vkEndCommandBuffer(command), "End command buffer"); check(vkResetFences(device, 1, &fence), "Reset fence"); - VkCommandBufferSubmitInfo cmd{VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO}; + VkCommandBufferSubmitInfo cmd{}; + cmd.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; cmd.commandBuffer = command; - VkSubmitInfo2 info{VK_STRUCTURE_TYPE_SUBMIT_INFO_2}; + VkSubmitInfo2 info{}; + info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; info.commandBufferInfoCount = 1; info.pCommandBufferInfos = &cmd; - VkSemaphoreSubmitInfo wait{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}, - signal{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}; + VkSemaphoreSubmitInfo wait{}, signal{}; + wait.sType = signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; if (present) { wait.semaphore = acquired; wait.stageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT; @@ -354,11 +367,12 @@ struct Renderer::Impl { std::cerr << "[Faset] Vulkan validation layer not installed; diagnostics disabled.\n"; if (validation) extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); - VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + VkApplicationInfo app{}; + app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; app.pApplicationName = "Faset Engine"; app.apiVersion = VK_API_VERSION_1_3; - VkDebugUtilsMessengerCreateInfoEXT debug_info{ - VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT}; + VkDebugUtilsMessengerCreateInfoEXT debug_info{}; + debug_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; debug_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; debug_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | @@ -367,7 +381,8 @@ struct Renderer::Impl { debug_info.pfnUserCallback = debug; debug_info.pUserData = this; const char* validation_name = "VK_LAYER_KHRONOS_validation"; - VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + VkInstanceCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; info.pApplicationInfo = &app; info.enabledExtensionCount = static_cast(extensions.size()); info.ppEnabledExtensionNames = extensions.data(); @@ -395,9 +410,10 @@ struct Renderer::Impl { vkGetPhysicalDeviceProperties(gpu, &properties); if (properties.apiVersion < VK_API_VERSION_1_3) continue; - VkPhysicalDeviceVulkan13Features f13{ - VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES}; - VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + VkPhysicalDeviceVulkan13Features f13{}; + f13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + VkPhysicalDeviceFeatures2 features{}; + features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features.pNext = &f13; vkGetPhysicalDeviceFeatures2(gpu, &features); if (!f13.synchronization2 || !f13.dynamicRendering) @@ -436,14 +452,17 @@ struct Renderer::Impl { throw std::runtime_error("No Vulkan 1.3 device supports dynamic rendering, " "synchronization2 and required color/depth formats"); float priority = 1; - VkDeviceQueueCreateInfo qi{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + VkDeviceQueueCreateInfo qi{}; + qi.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; qi.queueFamilyIndex = queue_family; qi.queueCount = 1; qi.pQueuePriorities = &priority; - VkPhysicalDeviceVulkan13Features f13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES}; + VkPhysicalDeviceVulkan13Features f13{}; + f13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; f13.synchronization2 = VK_TRUE; f13.dynamicRendering = VK_TRUE; - VkDeviceCreateInfo di{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + VkDeviceCreateInfo di{}; + di.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; di.pNext = &f13; di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi; @@ -454,23 +473,28 @@ struct Renderer::Impl { } check(vkCreateDevice(physical, &di, nullptr, &device), "Create Vulkan device"); vkGetDeviceQueue(device, queue_family, 0, &queue); - VkCommandPoolCreateInfo pi{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + VkCommandPoolCreateInfo pi{}; + pi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; pi.queueFamilyIndex = queue_family; pi.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; check(vkCreateCommandPool(device, &pi, nullptr, &pool), "Create command pool"); - VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + VkCommandBufferAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; ai.commandPool = pool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1; check(vkAllocateCommandBuffers(device, &ai, &command), "Allocate command buffer"); - VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFenceCreateInfo fi{}; + fi.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; check(vkCreateFence(device, &fi, nullptr, &fence), "Create frame fence"); - VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + VkSemaphoreCreateInfo si{}; + si.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; check(vkCreateSemaphore(device, &si, nullptr, &acquired), "Create acquire semaphore"); check(vkCreateSemaphore(device, &si, nullptr, &present_ready), "Create present semaphore"); if (timestamp_bits) { - VkQueryPoolCreateInfo query{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + VkQueryPoolCreateInfo query{}; + query.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; query.queryType = VK_QUERY_TYPE_TIMESTAMP; query.queryCount = 2; check(vkCreateQueryPool(device, &query, nullptr, ×tamp_pool), @@ -545,7 +569,8 @@ struct Renderer::Impl { count = caps.minImageCount + 1; if (caps.maxImageCount) count = std::min(count, caps.maxImageCount); - VkSwapchainCreateInfoKHR info{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; + VkSwapchainCreateInfoKHR info{}; + info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; info.surface = surface; info.minImageCount = count; info.imageFormat = chosen.format; @@ -595,21 +620,24 @@ struct Renderer::Impl { bindings[i].descriptorCount = 1; bindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } - VkDescriptorSetLayoutCreateInfo li{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + VkDescriptorSetLayoutCreateInfo li{}; + li.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; li.bindingCount = 4; li.pBindings = bindings.data(); check(vkCreateDescriptorSetLayout(device, &li, nullptr, &descriptor_layout), "Create descriptor layout"); VkDescriptorPoolSize sizes[] = {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2048}, {VK_DESCRIPTOR_TYPE_SAMPLER, 2048}}; - VkDescriptorPoolCreateInfo pi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + VkDescriptorPoolCreateInfo pi{}; + pi.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pi.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; pi.maxSets = 1024; pi.poolSizeCount = 2; pi.pPoolSizes = sizes; check(vkCreateDescriptorPool(device, &pi, nullptr, &descriptor_pool), "Create descriptor pool"); - VkSamplerCreateInfo si{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + VkSamplerCreateInfo si{}; + si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; si.magFilter = si.minFilter = VK_FILTER_NEAREST; si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; @@ -658,7 +686,8 @@ struct Renderer::Impl { VK_IMAGE_ASPECT_COLOR_BIT); submit(); destroy(staging); - VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + VkDescriptorSetAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; ai.descriptorPool = descriptor_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &descriptor_layout; @@ -671,7 +700,7 @@ struct Renderer::Impl { {color_sampler, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}}; std::array writes{}; for (std::uint32_t i = 0; i < 4; ++i) { - writes[i] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = texture.descriptor; writes[i].dstBinding = i; writes[i].descriptorCount = 1; @@ -700,16 +729,17 @@ struct Renderer::Impl { std::vector roots; const char* base = SDL_GetBasePath(); if (base) - roots.emplace_back(std::filesystem::path(base) / "shaders"); + roots.emplace_back(faset::path_from_utf8(base) / "shaders"); roots.emplace_back(std::filesystem::current_path() / "shaders"); - roots.emplace_back(FASET_SHADER_DIRECTORY); + roots.emplace_back(faset::path_from_utf8(FASET_SHADER_DIRECTORY)); for (const auto& root : roots) - if (std::filesystem::is_regular_file(root / "vertexMain.spv")) + if (std::filesystem::is_regular_file(faset::native_io_path(root / "vertexMain.spv"))) return root; throw std::runtime_error("Compiled Slang shader bundle is missing"); } VkShaderModule shader(const detail::ShaderCode& code) { - VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + VkShaderModuleCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; ci.codeSize = code.words.size() * sizeof(std::uint32_t); ci.pCode = code.words.data(); VkShaderModule result{}; @@ -724,7 +754,8 @@ struct Renderer::Impl { "Shader layout changed; the current pipeline was preserved"); VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(Push)}; - VkPipelineLayoutCreateInfo li{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + VkPipelineLayoutCreateInfo li{}; + li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; li.setLayoutCount = 1; li.pSetLayouts = &descriptor_layout; li.pushConstantRangeCount = 1; @@ -736,14 +767,14 @@ struct Renderer::Impl { vertex = shader(shaders[0]); fragment = shader(shaders[1]); shadow_vertex = shader(shaders[2]); - for (int mode = 0; mode < 3; ++mode) { - bool shadow_pass = mode == 2, ui = mode == 1; + for (int mode = 0; mode < 4; ++mode) { + bool shadow_pass = mode == 2, ui = mode == 1, sprite = mode == 3; VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = shadow_pass ? shadow_vertex : vertex; stages[0].pName = "main"; - stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment; stages[1].pName = "main"; @@ -756,20 +787,20 @@ struct Renderer::Impl { {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(GpuVertex, color)}, {4, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(GpuVertex, material)}, {5, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(GpuVertex, uv)}}; - VkPipelineVertexInputStateCreateInfo vi{ - VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineVertexInputStateCreateInfo vi{}; + vi.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; vi.vertexBindingDescriptionCount = 1; vi.pVertexBindingDescriptions = &binding; vi.vertexAttributeDescriptionCount = shadow_pass ? 1 : 6; vi.pVertexAttributeDescriptions = shadow_pass ? attrs + 1 : attrs; - VkPipelineInputAssemblyStateCreateInfo ia{ - VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo ia{}; + ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo vp{ - VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + VkPipelineViewportStateCreateInfo vp{}; + vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; vp.viewportCount = vp.scissorCount = 1; - VkPipelineRasterizationStateCreateInfo rs{ - VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + VkPipelineRasterizationStateCreateInfo rs{}; + rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; @@ -777,13 +808,13 @@ struct Renderer::Impl { rs.depthBiasEnable = shadow_pass; rs.depthBiasConstantFactor = 1.25f; rs.depthBiasSlopeFactor = 1.75f; - VkPipelineMultisampleStateCreateInfo ms{ - VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + VkPipelineMultisampleStateCreateInfo ms{}; + ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo ds{ - VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + VkPipelineDepthStencilStateCreateInfo ds{}; + ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; ds.depthTestEnable = !ui; - ds.depthWriteEnable = !ui; + ds.depthWriteEnable = !ui && !sprite; ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 15; @@ -794,22 +825,23 @@ struct Renderer::Impl { blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend.alphaBlendOp = VK_BLEND_OP_ADD; - VkPipelineColorBlendStateCreateInfo cb{ - VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + VkPipelineColorBlendStateCreateInfo cb{}; + cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; cb.attachmentCount = shadow_pass ? 0 : 1; cb.pAttachments = &blend; VkDynamicState states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; - VkPipelineDynamicStateCreateInfo dynamic{ - VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + VkPipelineDynamicStateCreateInfo dynamic{}; + dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = 2; dynamic.pDynamicStates = states; VkFormat format = VK_FORMAT_R8G8B8A8_UNORM; - VkPipelineRenderingCreateInfo rendering{ - VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO}; + VkPipelineRenderingCreateInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; rendering.colorAttachmentCount = shadow_pass ? 0 : 1; rendering.pColorAttachmentFormats = &format; rendering.depthAttachmentFormat = VK_FORMAT_D32_SFLOAT; - VkGraphicsPipelineCreateInfo pi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + VkGraphicsPipelineCreateInfo pi{}; + pi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pi.pNext = &rendering; pi.stageCount = shadow_pass ? 1 : 2; pi.pStages = stages; @@ -822,7 +854,10 @@ struct Renderer::Impl { pi.pColorBlendState = &cb; pi.pDynamicState = &dynamic; pi.layout = pipeline_layout; - auto* output = shadow_pass ? &shadow_pipeline : ui ? &ui_pipeline : &pipeline; + auto* output = shadow_pass ? &shadow_pipeline + : ui ? &ui_pipeline + : sprite ? &sprite_pipeline + : &pipeline; check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pi, nullptr, output), "Create graphics pipeline"); } @@ -999,7 +1034,7 @@ struct Renderer::Impl { if (sprite.texture) upload_texture(sprite.texture); std::vector data; - std::vector scene_batches, shadow_batches, ui_batches; + std::vector scene_batches, shadow_batches, sprite_batches, ui_batches; for (const auto& item : snapshot.draws) { if (!item.mesh) continue; @@ -1031,7 +1066,29 @@ struct Renderer::Impl { else scene_batches.push_back(batch); } + struct OrderedSprite { + const Sprite* sprite; + float depth; + }; + std::vector ordered_sprites; + ordered_sprites.reserve(snapshot.sprites.size()); for (const auto& sprite : snapshot.sprites) { + const auto clip = + point(snapshot.view_projection, + {sprite.position[0], sprite.position[1], sprite.position[2], 1}); + const auto depth = + clip[3] != 0 ? clip[2] / clip[3] : std::numeric_limits::infinity(); + ordered_sprites.push_back( + {&sprite, std::isfinite(depth) ? depth : std::numeric_limits::infinity()}); + } + std::stable_sort(ordered_sprites.begin(), ordered_sprites.end(), + [](const auto& a, const auto& b) { + if (a.sprite->layer != b.sprite->layer) + return a.sprite->layer < b.sprite->layer; + return a.depth > b.depth; + }); + for (const auto& ordered : ordered_sprites) { + const auto& sprite = *ordered.sprite; auto first = static_cast(data.size()); float c = std::cos(sprite.rotation), s = std::sin(sprite.rotation); for (auto i : {0, 1, 2, 0, 2, 3}) { @@ -1048,7 +1105,7 @@ struct Renderer::Impl { vertex.material[0] = sprite.texture && sprite.texture->srgb ? 1.f : 0.f; data.push_back(vertex); } - scene_batches.push_back( + sprite_batches.push_back( {first, 6, sprite.texture ? sprite.texture.get() : white.get()}); } for (const auto& q : snapshot.ui_quads) { @@ -1131,13 +1188,15 @@ struct Renderer::Impl { graph.add("ShadowMap", {}, {"shadow"}, [&] { transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); - VkRenderingAttachmentInfo attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; + VkRenderingAttachmentInfo attachment{}; + attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; attachment.imageView = shadow.view; attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; attachment.clearValue.depthStencil = {1, 0}; - VkRenderingInfo rendering{VK_STRUCTURE_TYPE_RENDERING_INFO}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {shadow_size, shadow_size}}; rendering.layerCount = 1; rendering.pDepthAttachment = &attachment; @@ -1160,20 +1219,23 @@ struct Renderer::Impl { VK_IMAGE_ASPECT_COLOR_BIT); transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); - VkRenderingAttachmentInfo ca{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; + VkRenderingAttachmentInfo ca{}; + ca.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; ca.imageView = color.view; ca.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; ca.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; ca.storeOp = VK_ATTACHMENT_STORE_OP_STORE; std::copy(snapshot.clear_color.begin(), snapshot.clear_color.end(), ca.clearValue.color.float32); - VkRenderingAttachmentInfo da{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; + VkRenderingAttachmentInfo da{}; + da.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; da.imageView = depth.view; da.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; da.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; da.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; da.clearValue.depthStencil = {1, 0}; - VkRenderingInfo rendering{VK_STRUCTURE_TYPE_RENDERING_INFO}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {width, height}}; rendering.layerCount = 1; rendering.colorAttachmentCount = 1; @@ -1205,6 +1267,14 @@ struct Renderer::Impl { vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline); + for (auto batch : sprite_batches) { + auto descriptor = textures.at(batch.texture).descriptor; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, + 0, 1, &descriptor, 0, nullptr); + vkCmdDraw(command, batch.count, 1, batch.first, 0); + ++statistics.draw_calls; + } set_viewport(width, height); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, ui_pipeline); vkCmdPushConstants(command, pipeline_layout, @@ -1262,7 +1332,8 @@ struct Renderer::Impl { statistics.gpu_ms = double(delta) * timestamp_period / 1000000.0; } if (swap_index) { - VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; + VkPresentInfoKHR present{}; + present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.waitSemaphoreCount = 1; present.pWaitSemaphores = &present_ready; present.swapchainCount = 1; @@ -1313,10 +1384,12 @@ bool Renderer::reload_shaders(std::string& error) { auto previous = r.pipeline; auto previous_ui = r.ui_pipeline; auto previous_shadow = r.shadow_pipeline; + auto previous_sprite = r.sprite_pipeline; r.pipeline_layout = {}; r.pipeline = {}; r.ui_pipeline = {}; r.shadow_pipeline = {}; + r.sprite_pipeline = {}; try { r.make_pipelines(); } catch (const std::exception& exception) { @@ -1326,18 +1399,22 @@ bool Renderer::reload_shaders(std::string& error) { vkDestroyPipeline(r.device, r.ui_pipeline, nullptr); if (r.shadow_pipeline) vkDestroyPipeline(r.device, r.shadow_pipeline, nullptr); + if (r.sprite_pipeline) + vkDestroyPipeline(r.device, r.sprite_pipeline, nullptr); if (r.pipeline_layout) vkDestroyPipelineLayout(r.device, r.pipeline_layout, nullptr); r.pipeline_layout = previous_layout; r.pipeline = previous; r.ui_pipeline = previous_ui; r.shadow_pipeline = previous_shadow; + r.sprite_pipeline = previous_sprite; error = exception.what(); return false; } vkDestroyPipeline(r.device, previous, nullptr); vkDestroyPipeline(r.device, previous_ui, nullptr); vkDestroyPipeline(r.device, previous_shadow, nullptr); + vkDestroyPipeline(r.device, previous_sprite, nullptr); vkDestroyPipelineLayout(r.device, previous_layout, nullptr); error.clear(); return true; @@ -1360,6 +1437,10 @@ std::uint32_t Renderer::width() const { std::uint32_t Renderer::height() const { return impl_->height; } +float Renderer::display_scale() const { + const float scale = impl_->window ? SDL_GetWindowDisplayScale(impl_->window) : 1.f; + return std::isfinite(scale) && scale > 0.f ? scale : 1.f; +} bool Renderer::should_close() const { return impl_->close; } @@ -1372,9 +1453,9 @@ std::vector Renderer::pixels() const { void Renderer::capture(const std::filesystem::path& path) { if (impl_->last_pixels.empty()) throw std::runtime_error("Cannot capture before a completed frame"); - std::ofstream out(path, std::ios::binary); + std::ofstream out(faset::native_io_path(path), std::ios::binary); if (!out) - throw std::runtime_error("Cannot write screenshot: " + path.string()); + throw std::runtime_error("Cannot write screenshot: " + faset::path_to_utf8(path)); out << "P6\n" << width() << ' ' << height() << "\n255\n"; for (std::size_t i = 0; i < impl_->last_pixels.size(); i += 4) out.write(reinterpret_cast(impl_->last_pixels.data() + i), 3); diff --git a/src/render/shader_contract.cpp b/src/render/shader_contract.cpp index 616582d..839335d 100644 --- a/src/render/shader_contract.cpp +++ b/src/render/shader_contract.cpp @@ -14,8 +14,10 @@ void require(bool value, const std::string& message) { throw std::runtime_error("Shader contract: " + message); } std::string read_bounded(const std::filesystem::path& path, std::uintmax_t maximum) { - require(std::filesystem::is_regular_file(path), "missing " + path.string()); - require(std::filesystem::file_size(path) <= maximum, "oversized " + path.string()); + const auto native = faset::native_io_path(path); + require(std::filesystem::is_regular_file(native), "missing " + faset::path_to_utf8(path)); + require(std::filesystem::file_size(native) <= maximum, + "oversized " + faset::path_to_utf8(path)); return faset::read_text(path); } void locations(const Json& fields, std::initializer_list types, const char* label) { diff --git a/src/runtime/Runtime.cpp b/src/runtime/Runtime.cpp index 9b4e7b6..d7fcff6 100644 --- a/src/runtime/Runtime.cpp +++ b/src/runtime/Runtime.cpp @@ -5,12 +5,19 @@ #include #include #include +#include #include #include #include #include namespace faset::runtime { +bool is_builtin_component(std::string_view type) noexcept { + constexpr std::array types{ + "faset.transform", "faset.sprite", "faset.mesh", "faset.camera", + "faset.light", "faset.rigid_body_2d", "faset.rigid_body_3d"}; + return std::find(types.begin(), types.end(), type) != types.end(); +} namespace { using Json = nlohmann::json; std::atomic nextSession{1}; @@ -20,6 +27,20 @@ void require(bool condition, const std::string& message) { if (!condition) throw std::invalid_argument(message); } +std::uint64_t schemaVersion(const Json& record, const std::string& context) { + if (!record.contains("version")) + return 1; + const auto& value = record.at("version"); + if (value.is_number_unsigned()) { + const auto version = value.get(); + require(version > 0, context + ": version must be a positive integer"); + return version; + } + require(value.is_number_integer(), context + ": version must be a positive integer"); + const auto version = value.get(); + require(version > 0, context + ": version must be a positive integer"); + return static_cast(version); +} template std::array vectorValue(const Json& object, const char* key, std::array fallback) { @@ -111,11 +132,13 @@ void validateEntity(const Json& entity, int dimension) { require(component.contains("type") && component["type"].is_string() && !component["type"].get().empty(), "component requires type"); - require(component.value("version", 1) == 1, "unsupported component version"); + const auto type = component["type"].get(); + const auto version = schemaVersion(component, "component " + type); + require(!is_builtin_component(type) || version == 1, + "unsupported builtin component version: " + type); require(component.contains("fields") && component["fields"].is_object(), "component requires fields"); require(ids.insert(component["id"].get()).second, "duplicate component id"); - const auto type = component["type"].get(); require(types.insert(type).second, "duplicate component type"); const auto& f = component["fields"]; if (type == "faset.transform") @@ -196,6 +219,52 @@ Transform interpolate(const Transform& a, const Transform& b, float alpha) { } } // namespace +void validate_scene_schemas(const Json& scene, const Json& gameplay_schema) { + require(gameplay_schema.is_array() || + (gameplay_schema.is_object() && gameplay_schema.contains("types") && + gameplay_schema.at("types").is_array()), + "gameplay schema must be an array or a types manifest"); + const auto& schemas = + gameplay_schema.is_array() ? gameplay_schema : gameplay_schema.at("types"); + std::unordered_map available; + for (const auto& schema : schemas) { + require(schema.is_object() && schema.contains("id") && schema.at("id").is_string() && + !schema.at("id").get().empty(), + "gameplay schema requires a nonempty TypeId"); + const auto id = schema.at("id").get(); + const auto version = schemaVersion(schema, "schema " + id); + require(!is_builtin_component(id), + "gameplay schema duplicates a builtin component TypeId: " + id); + require(available.emplace(id, version).second, "duplicate gameplay schema TypeId: " + id); + } + require(scene.is_object() && scene.contains("entities") && scene.at("entities").is_array(), + "scene entities must be an array"); + for (const auto& entity : scene.at("entities")) { + require(entity.is_object() && entity.contains("components") && + entity.at("components").is_array(), + "entity components must be an array"); + for (const auto& component : entity.at("components")) { + require(component.is_object() && component.contains("type") && + component.at("type").is_string() && + !component.at("type").get().empty(), + "component requires a nonempty TypeId"); + const auto type = component.at("type").get(); + const auto version = schemaVersion(component, "component " + type); + if (is_builtin_component(type)) { + require(version == 1, "unsupported builtin component version: " + type); + continue; + } + const auto found = available.find(type); + require(found != available.end(), + "component schema is absent from linked gameplay module: " + type); + require(found->second == version, "component schema version mismatch: " + type + + " (scene " + std::to_string(version) + + ", linked gameplay " + + std::to_string(found->second) + ")"); + } + } +} + struct Runtime::Impl { struct Data { Json document; diff --git a/src/ui/font.cpp b/src/ui/font.cpp index e29f3f4..bf146e3 100644 --- a/src/ui/font.cpp +++ b/src/ui/font.cpp @@ -1,3 +1,4 @@ +#include #include #include #include FT_FREETYPE_H @@ -6,6 +7,7 @@ #include #include #include +#include #include #include @@ -41,12 +43,16 @@ struct FontAtlas::Impl { std::map, Glyph> glyphs; unsigned x = 2, y = 2, row_height = 0, size = 0; explicit Impl(const std::filesystem::path& path) { - std::ifstream in(path, std::ios::binary | std::ios::ate); + // FreeType's Windows path backend uses CreateFileA. Read a native path + // ourselves and retain its bytes until hb_font/FT_Face are destroyed. + std::ifstream in(native_io_path(path), std::ios::binary | std::ios::ate); if (!in) - throw std::runtime_error("Cannot open UI font: " + path.string()); + throw std::runtime_error("Cannot open UI font: " + path_to_utf8(path)); auto length = in.tellg(); if (length <= 0) throw std::runtime_error("Empty UI font"); + if (length > std::numeric_limits::max()) + throw std::runtime_error("UI font exceeds FreeType's supported buffer size"); font_bytes.resize(static_cast(length)); in.seekg(0); in.read(reinterpret_cast(font_bytes.data()), length); diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 4e54ade..a74c188 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -739,9 +739,26 @@ void Context::apply_layout(const Json& document) { apply(root(), definition); } void Context::layout(float width, float height, float scale) { + if (!std::isfinite(width) || !std::isfinite(height) || !std::isfinite(scale) || scale <= 0) + throw std::invalid_argument("UI dimensions and display scale must be finite and valid"); + scale = std::clamp(scale, .5f, 4.f); + if (impl_->scale != scale) { + // Pointer captures use the old drawable coordinate system. Keep edits, + // but cancel a drag rather than committing a jump after a monitor change. + impl_->cancel_capture(); + const auto ratio = scale / impl_->scale; + std::function rescale = [&](Widget& widget) { + widget.scroll_y *= ratio; + for (auto& child : widget.children) + rescale(*child); + }; + rescale(root()); + for (auto& [id, edit] : impl_->edits) + edit.scroll *= ratio; + } impl_->width = std::max(0.f, width); impl_->height = std::max(0.f, height); - impl_->scale = std::clamp(scale, .5f, 4.f); + impl_->scale = scale; std::set ids; std::function check = [&](Widget& w) { if (w.id.empty() || !ids.insert(w.id).second) diff --git a/tests/assets_pipeline.cpp b/tests/assets_pipeline.cpp index 882d3bf..01966cb 100644 --- a/tests/assets_pipeline.cpp +++ b/tests/assets_pipeline.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -102,13 +103,15 @@ void success(const ImportResult& result) { } } // namespace int main() { - const auto root = fs::temp_directory_path() / - ("faset-assets-test-" + - std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto root = + fs::temp_directory_path() / + faset::path_from_utf8( + "Faset Café 世界 assets " + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); fs::create_directories(root); try { AssetPipeline pipeline(root / "cache"); - const auto source = root / "door.glb"; + const auto source = root / faset::path_from_utf8("дверь 世界.glb"); glb(source); auto first = pipeline.import_asset({source}); success(first); @@ -171,6 +174,19 @@ int main() { require(pipeline.overrides(first.asset_id) == custom, "conflict erased overrides"); ImportRequest resolve{source}; resolve.allow_removed_outputs = true; + resolve.expected_generation = removed.generation; + resolve.expected_active_generation = added.generation; + glb(source, "Renamed panel", true, false, .5f); + auto changed_since_review = pipeline.import_asset(resolve); + require(changed_since_review.status == ImportStatus::conflict && + pipeline.load_asset(first.asset_id).generation == added.generation, + "review approval must not publish a changed candidate"); + glb(source, "Renamed panel", true, false, .25f); + resolve.expected_active_generation = first.generation; + require(pipeline.import_asset(resolve).status == ImportStatus::conflict && + pipeline.load_asset(first.asset_id).generation == added.generation, + "review approval must not replace a different active generation"); + resolve.expected_active_generation = added.generation; auto resolved = pipeline.import_asset(resolve); success(resolved); require(pipeline.load_asset(first.asset_id).nodes.size() == 1, @@ -207,14 +223,14 @@ int main() { "ambiguous rename silently matched"); auto duplicate = document("Duplicate", true, true); duplicate["nodes"][1]["extras"]["faset_id"] = "node-door"; - duplicate["buffers"][0]["uri"] = "mesh.bin"; - save(root / "mesh.bin", geometry(0)); + duplicate["buffers"][0]["uri"] = "геометрия.bin"; + save(root / faset::path_from_utf8("геометрия.bin"), geometry(0)); save(root / "duplicate.gltf", duplicate.dump()); require(pipeline.import_asset({root / "duplicate.gltf"}).status == ImportStatus::failed, "duplicate source ID accepted"); auto external = document("External", true, false); - external["buffers"][0]["uri"] = "mesh.bin"; - external["images"] = Json::array({{{"uri", "pixel.png"}, {"mimeType", "image/png"}}}); + external["buffers"][0]["uri"] = "геометрия.bin"; + external["images"] = Json::array({{{"uri", "пиксель.png"}, {"mimeType", "image/png"}}}); external["textures"] = Json::array({{{"source", 0}}}); external["materials"][0]["pbrMetallicRoughness"]["baseColorTexture"] = {{"index", 0}}; // Real 1x1 PNG payload, transparent pixel; importer owns encoded bytes. @@ -223,7 +239,7 @@ int main() { 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1, 165, 246, 69, 64, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130}; - save(root / "pixel.png", png); + save(root / faset::path_from_utf8("пиксель.png"), png); save(root / "external.gltf", external.dump()); auto ext = pipeline.import_asset({root / "external.gltf"}); success(ext); @@ -231,7 +247,7 @@ int main() { require(ext_asset.textures.size() == 1 && ext_asset.textures[0].bytes.size() == png.size(), "external image not extracted"); require(ext_asset.materials[0].base_color_texture == 0, "material texture reference lost"); - save(root / "mesh.bin", geometry(.3f)); + save(root / faset::path_from_utf8("геометрия.bin"), geometry(.3f)); auto dependent = pipeline.import_asset({root / "external.gltf"}); success(dependent); require(dependent.generation != ext.generation, "buffer dependency not invalidated"); @@ -269,7 +285,7 @@ int main() { const auto dependency_active = pipeline.load_asset(ext.asset_id).generation; ImportJob mutate([&](const ImportProgress& progress) { if (progress.fraction == .5f) - save(root / "mesh.bin", geometry(.7f)); + save(root / faset::path_from_utf8("геометрия.bin"), geometry(.7f)); }); require(pipeline.import_asset({root / "external.gltf"}, mutate).status == ImportStatus::failed, @@ -277,7 +293,7 @@ int main() { require(pipeline.load_asset(ext.asset_id).generation == dependency_active, "concurrent edit changed active"); // Standalone images share the same identity, generation and failure guarantees. - const auto picture = root / "sprite.PNG"; + const auto picture = root / faset::path_from_utf8("спрайт Café.PNG"); save(picture, faset::test_images::png_red_green); ImportRequest image_request{picture}; image_request.settings = {{"pixels_per_unit", 20.0}}; @@ -345,15 +361,16 @@ int main() { save(picture, faset::test_images::png_blue_white); const auto renamed = root / "renamed.PNG"; fs::rename(picture, renamed); - fs::rename(picture.string() + ".faset-import.json", - renamed.string() + ".faset-import.json"); - fs::rename(picture.string() + ".faset-overrides.json", - renamed.string() + ".faset-overrides.json"); + fs::rename(faset::path_from_utf8(faset::path_to_utf8(picture) + ".faset-import.json"), + faset::path_from_utf8(faset::path_to_utf8(renamed) + ".faset-import.json")); + fs::rename(faset::path_from_utf8(faset::path_to_utf8(picture) + ".faset-overrides.json"), + faset::path_from_utf8(faset::path_to_utf8(renamed) + ".faset-overrides.json")); auto image_rename = pipeline.import_asset({renamed}); success(image_rename); require(image_rename.cache_hit && image_rename.asset_id == image_first.asset_id, "Image source rename with sidecar lost identity/cache"); - require(pipeline.current_manifest(image_first.asset_id)["source"] == renamed.string(), + require(pipeline.current_manifest(image_first.asset_id)["source"] == + faset::path_to_utf8(renamed), "Image rename retained old source pointer"); require(pipeline.overrides(image_first.asset_id).contains(texture_id), "Image rename/reimport lost overrides"); diff --git a/tests/build_service_tests.cpp b/tests/build_service_tests.cpp index 304aef9..b965cc1 100644 --- a/tests/build_service_tests.cpp +++ b/tests/build_service_tests.cpp @@ -11,6 +11,12 @@ #ifndef _WIN32 #include #endif +#ifdef __linux__ +#include +#include +#include +#include +#endif using namespace faset; namespace fs = std::filesystem; void require(bool value, const char* message) { @@ -38,7 +44,7 @@ int integration(const fs::path& root) { fs::create_directories(root); editor::BuildConfig config; config.project_root = root / "project"; - config.engine_root = FASET_ENGINE_SOURCE; + config.engine_root = path_from_utf8(FASET_ENGINE_SOURCE); config.build_directory = root / "native-build"; config.cache_root = root / "project" / ".faset" / "cache"; editor::BuildService service(config); @@ -114,18 +120,19 @@ int integration(const fs::path& root) { {"components", components}}); auto result = wait(service.start_export(document, root / ("export-" + std::to_string(dimension)))); - const auto directory = fs::path(result.result.at("directory").get()); + const auto directory = path_from_utf8(result.result.at("directory").get()); require(result.result.at("configuration") == "Release", "Exports default to the Release profile"); - require(fs::path(result.result.at("build_directory").get()) == + require(path_from_utf8(result.result.at("build_directory").get()) == config.build_directory / "Release", "Export has a separate CMake directory"); require(read_json(directory / "manifest.json").at("configuration") == "Release", "Export manifest records the actual profile"); - Process player({{result.result.at("executable").get(), "--headless", - "--frames", "3", "--capture", (directory / "verification.ppm").string()}, - directory, - {}}); + Process player( + {{result.result.at("executable").get(), "--headless", "--frames", "3", + "--capture", path_to_utf8(directory / "verification.ppm")}, + directory, + {}}); std::cout << collect(player); require(fs::file_size(directory / "verification.ppm") > 1000, "Exported game rendered a frame"); @@ -139,12 +146,12 @@ int integration(const fs::path& root) { auto release_cache = read_text(config.build_directory / "Release" / "CMakeCache.txt"); auto rebuilt = wait(service.start_build()); require(rebuilt.result.at("configuration") == "Debug", "Development builds remain Debug"); - require(fs::path(rebuilt.result.at("build_directory").get()) == + require(path_from_utf8(rebuilt.result.at("build_directory").get()) == config.build_directory / "Debug", "Development CMake directory is isolated"); require(read_text(config.build_directory / "Release" / "CMakeCache.txt") == release_cache, "Development build preserves the Release cache"); - auto schema = read_json(rebuilt.result.at("schema").get()); + auto schema = read_json(path_from_utf8(rebuilt.result.at("schema").get())); bool updated{}; for (const auto& type : schema.at("types")) if (type.value("name", "") == "Custom Character") @@ -162,13 +169,104 @@ int integration(const fs::path& root) { "schema and failed-build recovery passed\n"; return 0; } -int main(int argc, char** argv) { +#ifdef __linux__ +void descendant_cleanup(const fs::path& executable, const fs::path& directory) { + struct Subreaper { + int previous{}; + Subreaper() { + require(::prctl(PR_GET_CHILD_SUBREAPER, &previous) == 0 && + ::prctl(PR_SET_CHILD_SUBREAPER, 1) == 0, + "Install test-only descendant reaper"); + } + ~Subreaper() { + ::prctl(PR_SET_CHILD_SUBREAPER, previous); + } + } subreaper; + for (bool cancel : {false, true}) { + Process process( + {{path_to_utf8(executable), cancel ? "--tree-sleep" : "--tree-exit"}, directory, {}}); + std::string output; + bool stopped{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (std::chrono::steady_clock::now() < deadline) { + auto state = process.poll(); + output += state.output; + if (cancel && output.find("leader-tail\n") != std::string::npos) { + process.cancel(); + state = process.poll(); + output += state.output; + } + if (!state.running) { + require(cancel || state.exit_code == 0, "Leader exit status remains intact"); + stopped = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + require(stopped && output.find("leader-tail\n") != std::string::npos, + "Leader exit and buffered stdout tail are observed"); + const auto descendant = static_cast(std::stol(output)); + int status{}; + pid_t reaped{}; + const auto reap_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + reaped = ::waitpid(descendant, &status, WNOHANG); + if (reaped != 0) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } while (std::chrono::steady_clock::now() < reap_deadline); + const bool cleaned = + reaped == descendant && WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL; + if (reaped == 0) { + // Clean up the still-owned child even when this regression fails. + ::kill(descendant, SIGKILL); + while (::waitpid(descendant, &status, 0) < 0 && errno == EINTR) { + } + } + require(cleaned, "Leader completion terminates its SIGTERM-resistant descendant"); + process.cancel(); // Completed ownership must not signal any stale process-group ID. + } +} +#endif +int test_main(int argc, char** argv) { +#ifdef __linux__ + if (argc > 1 && + (std::string(argv[1]) == "--tree-exit" || std::string(argv[1]) == "--tree-sleep")) { + int ready[2]; + if (::pipe(ready) != 0) + return 2; + const auto descendant = ::fork(); + if (descendant < 0) + return 3; + if (descendant == 0) { + ::close(ready[0]); + std::signal(SIGTERM, SIG_IGN); + const char byte = 'r'; + if (::write(ready[1], &byte, 1) != 1) + ::_exit(4); + ::close(ready[1]); + while (true) + ::pause(); + } + ::close(ready[1]); + char byte{}; + const auto count = ::read(ready[0], &byte, 1); + ::close(ready[0]); + if (count != 1) + return 5; + std::cout << descendant << "\nleader-tail\n" << std::flush; + if (std::string(argv[1]) == "--tree-sleep") + while (true) + ::pause(); + return 0; + } +#endif if (argc > 1 && std::string(argv[1]) == "--child") { Json args = Json::array(); for (int i = 2; i < argc; ++i) args.push_back(argv[i]); std::cout << Json{{"args", args}, - {"cwd", fs::current_path().string()}, + {"cwd", path_to_utf8(fs::current_path())}, {"env", std::getenv("FASET_PROCESS_TEST") ? std::getenv("FASET_PROCESS_TEST") : ""}} @@ -188,32 +286,35 @@ int main(int argc, char** argv) { } if (argc == 3 && std::string(argv[1]) == "--integration") { try { - return integration(fs::absolute(argv[2])); + return integration(fs::absolute(path_from_utf8(argv[2]))); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } - fs::path temporary = fs::temp_directory_path() / ("Faset build test " + new_id()); + fs::path temporary = fs::temp_directory_path() / path_from_utf8("Faset Café 世界 " + new_id()); try { fs::create_directories(temporary); - auto executable = fs::absolute(argv[0]); - Process child({{executable.string(), "--child", "space argument", "quote\"backslash\\", - "$(touch not-executed); & |", ""}, + const auto original_executable = fs::absolute(path_from_utf8(argv[0])); + const auto executable = temporary / original_executable.filename(); + fs::copy_file(original_executable, executable); + Process child({{path_to_utf8(executable), "--child", "space argument", "quote\"backslash\\", + "$(touch not-executed); & |", "", "Café 世界 Привет 😀"}, temporary, {{"FASET_PROCESS_TEST", "value with spaces"}}}); auto text = collect(child); auto result = Json::parse(text.substr(0, text.find('\n'))); - require(result["args"] == Json::array({"space argument", "quote\"backslash\\", - "$(touch not-executed); & |", ""}), + require(result["args"] == + Json::array({"space argument", "quote\"backslash\\", + "$(touch not-executed); & |", "", "Café 世界 Привет 😀"}), "Arguments must remain literal"); require(result["env"] == "value with spaces", "Child environment override"); - require(fs::equivalent(result["cwd"].get(), temporary), + require(fs::equivalent(path_from_utf8(result["cwd"].get()), temporary), "Child working directory"); require(text.find("stderr-sentinel") != std::string::npos && text.size() > 100000, "Combined pipe output drained fully"); require(!fs::exists(temporary / "not-executed"), "No shell execution"); - Process sleeper({{executable.string(), "--sleep"}, temporary, {}}); + Process sleeper({{path_to_utf8(executable), "--sleep"}, temporary, {}}); bool ready{}; while (!ready) { auto p = sleeper.poll(); @@ -226,9 +327,12 @@ int main(int argc, char** argv) { require(!sleeper.poll().running, "Cancellation must reap the process"); require(std::chrono::steady_clock::now() - start < std::chrono::seconds(3), "Cancellation must finish promptly"); +#ifdef __linux__ + descendant_cleanup(executable, temporary); +#endif editor::BuildConfig config; config.project_root = temporary / "project"; - config.engine_root = FASET_ENGINE_SOURCE; + config.engine_root = path_from_utf8(FASET_ENGINE_SOURCE); editor::BuildService service(config); service.scaffold("Test project", 2); atomic_write(config.project_root / "Scripts" / "Gameplay.cpp", "// User code\n"); @@ -238,7 +342,7 @@ int main(int argc, char** argv) { auto id = service.start_cook(scene(2)); auto cooked = service.wait(id); require(cooked.state == "succeeded", "Cook job succeeds"); - auto bytes = read_text(cooked.result.at("scene").get()); + auto bytes = read_text(path_from_utf8(cooked.result.at("scene").get())); require(bytes.substr(0, 8) == "FASETSCN" && bytes.size() > 20, "Cooked envelope magic"); std::uint32_t version{}; std::uint64_t size{}; @@ -287,3 +391,13 @@ int main(int argc, char** argv) { return 1; } } + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return faset::run_utf8_main(argc, argv, test_main); +} +#else +int main(int argc, char** argv) { + return test_main(argc, argv); +} +#endif diff --git a/tests/core_tests.cpp b/tests/core_tests.cpp index ed3b618..31ac747 100644 --- a/tests/core_tests.cpp +++ b/tests/core_tests.cpp @@ -33,6 +33,44 @@ int main() { faset::sha256(faset::read_text(directory / "state.json"))); CHECK(std::filesystem::equivalent(faset::project_path(directory, "assets/../state.json"), directory / "state.json")); + const auto unicode = directory / faset::path_from_utf8("Faset Café 世界/Сцена 😀.json"); + faset::atomic_write_json(unicode, {{"hello", "Unicode paths"}}); + faset::atomic_write_json(unicode, {{"hello", "Replaced"}}); + CHECK(faset::read_json(unicode).at("hello") == "Replaced"); + CHECK(faset::sha256_file(unicode) == faset::sha256(faset::read_text(unicode))); + CHECK(faset::path_from_utf8(faset::path_to_utf8(unicode)) == unicode); + CHECK(faset::generic_path_to_utf8(unicode.lexically_relative(directory)) == + "Faset Café 世界/Сцена 😀.json"); + CHECK(std::filesystem::equivalent( + faset::project_path(directory, faset::path_from_utf8("Faset Café 世界/Сцена 😀.json")), + unicode)); + for (const auto& invalid : + std::vector{std::string("a\0b", 3), "\xc0\xaf", "\xed\xa0\x80", + "\xf4\x90\x80\x80", "\xe2\x82", "\x80"}) { + bool invalid_rejected = false; + try { + faset::path_from_utf8(invalid); + } catch (const faset::Error& error) { + invalid_rejected = error.json().at("code") == "path.encoding"; + } + CHECK(invalid_rejected); + } + auto deep = directory / faset::path_from_utf8("Long Café 世界"); + while (deep.native().size() < 310) + deep /= "directory-segment-0123456789"; + const auto long_file = deep / faset::path_from_utf8("Сцена 世界.json"); + faset::atomic_write_json(long_file, {{"long_path", true}}); + CHECK(faset::read_json(long_file).at("long_path") == true); + CHECK(faset::sha256_file(long_file) == faset::sha256(faset::read_text(long_file))); + CHECK(std::filesystem::is_regular_file(faset::native_io_path(long_file))); + CHECK(faset::path_to_utf8(long_file).find("\\\\?\\") == std::string::npos); +#ifdef _WIN32 + CHECK(faset::native_io_path(long_file).native().starts_with(LR"(\\?\)")); + CHECK(faset::native_io_path(std::filesystem::path(LR"(\\server\share\file.txt)")) == + std::filesystem::path(LR"(\\?\UNC\server\share\file.txt)")); + CHECK(faset::native_io_path(faset::native_io_path(long_file)) == + faset::native_io_path(long_file)); +#endif bool rejected = false; try { faset::project_path(directory, "../escape"); @@ -47,12 +85,12 @@ int main() { rejected = true; } CHECK(rejected); - std::filesystem::remove_all(directory); + std::filesystem::remove_all(faset::native_io_path(directory)); std::cout << "Core: SHA-256 vectors, persistent IDs, durable replace, Unicode, path " "boundaries passed\n"; return 0; } catch (const std::exception& error) { - std::filesystem::remove_all(directory); + std::filesystem::remove_all(faset::native_io_path(directory)); std::cerr << error.what() << '\n'; return 1; } diff --git a/tests/editor_blender_reimport_probe.cpp b/tests/editor_blender_reimport_probe.cpp new file mode 100644 index 0000000..82d6708 --- /dev/null +++ b/tests/editor_blender_reimport_probe.cpp @@ -0,0 +1,144 @@ +#include +#include +#include +#include +#include + +using namespace faset; +namespace fs = std::filesystem; +namespace { +void check(bool value, const char* message) { + if (!value) + throw std::runtime_error(message); +} +Json geometry(const render::DrawItem& draw) { + Json positions = Json::array(); + for (const auto& vertex : draw.mesh->vertices) + positions.push_back(vertex.position); + return positions; +} +int run(int argc, char** argv) { + try { + check(argc == 3, "Usage: faset_blender_editor_probe FIXTURE_DIRECTORY NEW_PROJECT"); + const auto fixture = path_from_utf8(argv[1]), project = path_from_utf8(argv[2]); + check(!fs::exists(project), "Use a new project directory for the Blender UI probe"); + const auto source = project / "Assets/door"; + fs::create_directories(source); + editor::Session session({project, path_from_utf8(FASET_TEST_ENGINE), project}); + render::Renderer renderer({.width = 1280, + .height = 900, + .title = "Blender reimport", + .headless = true, + .validation = true}); + editor::EditorUI ui(session, renderer, + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + auto import = [&](const char* stage) { + fs::copy(fixture / stage, source, + fs::copy_options::recursive | fs::copy_options::overwrite_existing); + const auto job = session.commands() + .call("faset_import", {{"path", "Assets/door/manifest.json"}}) + .at("job"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30); + while (std::chrono::steady_clock::now() < deadline) { + ui.frame({}); + auto result = session.commands().call("faset_job", {{"id", job}}); + if (result.at("state") != "queued" && result.at("state") != "running") + return result; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + throw std::runtime_error("Blender import timed out"); + }; + const auto initial = import("initial"); + check(initial.at("state") == "succeeded", "Initial Blender import failed"); + const auto id = initial.at("result").at("asset_id"); + const auto document = ui.current_document(); + auto current = session.authoring().query(document); + Json operations = Json::array(); + for (int index = 0; index < 2; ++index) { + auto entity = authoring::make_entity(session.authoring().schemas(), "Placed door"); + entity["components"][0]["fields"]["position"] = {index ? 3 : -3, 0, 0}; + entity["components"].push_back( + {{"id", new_id()}, + {"type", "faset.mesh"}, + {"version", 1}, + {"fields", + {{"asset", id}, + {"primitive", "asset"}, + {"color", index ? Json{0.5, 0.8, 0.3, 1} : Json{0.3, 0.5, 0.9, 1}}}}}); + // Missing gameplay packages stay opaque; reimport must not remove them. + entity["components"].push_back({{"id", new_id()}, + {"type", "project.door"}, + {"version", 2}, + {"fields", {{"locked", index == 0}, {"speed", 2.5}}}}); + auto body = session.authoring().schemas().default_fields("faset.rigid_body_3d"); + body["body_type"] = "static"; + body["friction"] = index ? 0.75 : 0.25; + entity["components"].push_back({{"id", new_id()}, + {"type", "faset.rigid_body_3d"}, + {"version", 1}, + {"fields", body}}); + operations.push_back({{"op", "entity.create"}, {"entity", entity}}); + } + session.authoring().transact(document, current.at("revision"), operations); + current = session.authoring().query(document); + ui.frame({}); + check(ui.snapshot().draws.size() >= 2, "Both placed Blender meshes must render"); + const auto first = ui.snapshot().draws[0], second = ui.snapshot().draws[1]; + check(first.mesh == second.mesh && first.model != second.model, + "Two independent placements must share the imported mesh"); + renderer.render(ui.snapshot()); + renderer.capture(project / "initial.ppm"); + const auto renamed = import("renamed"); + check(renamed.at("state") == "succeeded", "Renamed Blender import failed"); + // Exercise the real editor's generation watcher, without manually clearing SceneView. + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + ui.frame({}); + const auto first_after = ui.snapshot().draws[0], second_after = ui.snapshot().draws[1]; + check(geometry(first) != geometry(first_after) && + geometry(second) != geometry(second_after), + "Both live Editor instances must receive changed Blender geometry"); + check(first_after.mesh == second_after.mesh, "New instances share one cooked mesh"); + check(first.model == first_after.model && second.model == second_after.model && + first.color == first_after.color && second.color == second_after.color, + "Reimport must preserve each instance's placement and tint"); + check(session.authoring().query(document) == current, + "Reimport must preserve authoring IDs, gameplay fields and revision"); + renderer.render(ui.snapshot()); + renderer.capture(project / "renamed.ppm"); + const auto removed = import("removed"); + check(removed.at("state") == "conflict", "Removed Blender output must conflict"); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + ui.frame({}); + check(geometry(ui.snapshot().draws[0]) == geometry(first_after) && + geometry(ui.snapshot().draws[1]) == geometry(second_after), + "Conflict must keep both last-good visible meshes"); + check(session.authoring().query(document) == current, + "Import conflict must preserve authoring"); + check(renderer.stats().validation_errors == 0, "Vulkan validation failed"); + atomic_write_json(project / "probe.json", + {{"status", "passed"}, + {"instances", 2}, + {"live_geometry_updated", true}, + {"placement_and_tint_preserved", true}, + {"opaque_gameplay_preserved", true}, + {"physics_fields_preserved", true}, + {"conflict_keeps_visible_generation", true}, + {"validation_enabled", renderer.stats().validation_enabled}, + {"validation_errors", renderer.stats().validation_errors}}); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} +} // namespace +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return run_utf8_main(argc, argv, run); +} +#else +int main(int argc, char** argv) { + return run(argc, argv); +} +#endif diff --git a/tests/editor_session_tests.cpp b/tests/editor_session_tests.cpp index 77bcf72..424345e 100644 --- a/tests/editor_session_tests.cpp +++ b/tests/editor_session_tests.cpp @@ -4,12 +4,21 @@ int main() { using namespace faset; - const auto root = std::filesystem::temp_directory_path() / ("faset-session-" + new_id()); + const auto root = std::filesystem::temp_directory_path() / + path_from_utf8("Faset Café 世界 session " + new_id()); try { - editor::Session session({root, FASET_TEST_ENGINE, {}}); + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), {}}); session.scaffold("Settings", 3); auto document = session.authoring().create("Start", 3); - session.authoring().save(document.at("id"), "Scenes/main.scene.json"); + session.commands().call("faset_document_save", {{"document", document.at("id")}, + {"path", "Scenes/Начало 世界.scene.json"}}); + require(session.authoring().query(document.at("id")).at("path") == + "Scenes/Начало 世界.scene.json", + "test", "Saved path must round-trip as UTF-8"); + require(std::filesystem::is_regular_file(root / + path_from_utf8("Scenes/Начало 世界.scene.json")), + "test", "Unicode scene was saved under the wrong filename"); + session.commands().call("faset_document_open", {{"path", "Scenes/Начало 世界.scene.json"}}); auto& commands = session.commands(); const auto initial = commands.call("faset_project_settings_get", Json::object()); auto changed = commands.call("faset_project_settings_set", @@ -17,7 +26,7 @@ int main() { {"settings", {{"name", "Проект 世界"}, {"dimension", 2}, - {"start_scene", "Scenes/main.scene.json"}}}}); + {"start_scene", "Scenes/Начало 世界.scene.json"}}}}); require(changed.at("settings").at("name") == "Проект 世界" && session.project().at("dimension") == 2, "test", "Project settings were not saved"); diff --git a/tests/editor_ui_gizmos.cpp b/tests/editor_ui_gizmos.cpp index 90fd246..a7961bd 100644 --- a/tests/editor_ui_gizmos.cpp +++ b/tests/editor_ui_gizmos.cpp @@ -43,11 +43,11 @@ void near(float a, float b, const char* message) { int main() { const auto root = std::filesystem::temp_directory_path() / ("faset-gizmo-ui-" + new_id()); try { - editor::Session session({root, FASET_TEST_ENGINE, root}); + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); render::Renderer renderer({1280, 900, "Parented gizmo acceptance", true, true}); editor::EditorUI ui(session, renderer, - std::filesystem::path(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", - std::filesystem::path(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); ui.frame({}); auto query = [&] { return session.authoring().query(ui.current_document()); }; auto parent = @@ -158,10 +158,10 @@ int main() { check(renderer.stats().validation_errors == 0, "Vulkan validation"); std::cout << "Parented world Move/local Rotate/local Scale, multi-frame preview, Undo, " "Escape and composed local rotation passed. " - << root << '\n'; + << path_to_utf8(root) << '\n'; return 0; } catch (const std::exception& error) { - std::cerr << error.what() << "\nRetained: " << root << '\n'; + std::cerr << error.what() << "\nRetained: " << path_to_utf8(root) << '\n'; return 1; } } diff --git a/tests/editor_ui_import_conflicts.cpp b/tests/editor_ui_import_conflicts.cpp new file mode 100644 index 0000000..93a26a1 --- /dev/null +++ b/tests/editor_ui_import_conflicts.cpp @@ -0,0 +1,167 @@ +#include +#include +#include +#include +#include +#include +using namespace faset; +namespace { +void check(bool value, const std::string& message) { + if (!value) + throw std::runtime_error(message); +} +void click(editor::EditorUI& ui, const std::string& id) { + for (int attempt = 0; attempt < 40; ++attempt) { + const auto* w = ui.widgets().find(id); + check(w, "Missing widget: " + id); + const auto visible = w->rect.intersection(w->clip); + if (visible.width > 2 && visible.height > 2) { + render::Event down; + down.type = render::Event::Type::MouseDown; + down.button = 1; + down.x = visible.x + visible.width * .5f; + down.y = visible.y + visible.height * .5f; + auto up = down; + up.type = render::Event::Type::MouseUp; + ui.frame({down, up}); + return; + } + auto* parent = w->parent; + while (parent && !parent->layout.scroll) + parent = parent->parent; + check(parent, "Cannot scroll to " + id); + render::Event move, wheel; + move.type = render::Event::Type::MouseMove; + move.x = parent->rect.x + 10; + move.y = parent->rect.y + 10; + wheel.type = render::Event::Type::Wheel; + wheel.y = w->rect.y < parent->rect.y ? 1 : -1; + ui.frame({move, wheel}); + } + throw std::runtime_error("Widget remains clipped: " + id); +} +Json job(editor::Session& session, const std::string& id) { + return session.commands().call("faset_job", {{"id", id}}); +} +Json wait(editor::Session& session, editor::EditorUI& ui, const std::string& id) { + for (int i = 0; i < 1000; ++i) { + ui.frame({}); + const auto current = job(session, id); + if (current.at("state") != "queued" && current.at("state") != "running") + return current; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + throw std::runtime_error("Import did not finish"); +} +std::set jobs(editor::Session& session) { + std::set result; + const auto response = session.commands().call("faset_jobs", Json::object()); + for (const auto& value : response.at("jobs")) + result.insert(value.at("id")); + return result; +} +std::string new_job(editor::Session& session, const std::set& before) { + for (const auto& id : jobs(session)) + if (!before.contains(id)) + return id; + throw std::runtime_error("UI did not submit an import job"); +} +} // namespace +int main() { + const auto root = std::filesystem::temp_directory_path() / ("faset-import-ui-" + new_id()); + try { + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); + render::Renderer renderer({1280, 900, "Import conflict workflow", true, true}); + editor::EditorUI ui(session, renderer, + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + const auto source = root / "Assets/model.gltf"; + Json gltf = { + {"asset", {{"version", "2.0"}}}, + {"scene", 0}, + {"scenes", Json::array({{{"nodes", {0, 1}}}})}, + {"nodes", + Json::array({{{"name", "Keep"}, {"extras", {{"faset_id", new_id()}}}}, + {{"name", "Removed node"}, {"extras", {{"faset_id", new_id()}}}}})}}; + atomic_write_json(source, gltf); + const auto initial_id = session.commands() + .call("faset_import", {{"path", "Assets/model.gltf"}, + {"settings", {{"units", "metres"}}}}) + .at("job") + .get(); + const auto initial = wait(session, ui, initial_id); + check(initial.at("state") == "succeeded", "Fixture initial import: " + initial.dump()); + const auto asset_id = initial.at("result").at("asset_id").get(); + const auto initial_generation = initial.at("result").at("generation"); + assets::AssetStore store(root / ".faset/cache"); + const auto document_before = session.authoring().query(ui.current_document()); + gltf["nodes"].erase(gltf["nodes"].end() - 1); + gltf["scenes"][0]["nodes"] = Json::array({0}); + atomic_write_json(source, gltf); + click(ui, "tab-assets"); + click(ui, "asset-refresh"); + click(ui, "file-Assets/model.gltf"); + auto before = jobs(session); + click(ui, "asset-import"); + const auto conflict_id = new_job(session, before); + auto conflict = wait(session, ui, conflict_id); + check(conflict.at("state") == "conflict", "Deleted output must remain a conflict"); + click(ui, "tab-jobs"); + click(ui, "job-review-" + conflict_id); + const auto group = "import-conflict-" + conflict_id; + const auto removed_id = conflict["result"]["removed_output_ids"][0].get(); + const auto* output = ui.widgets().find(group + "-output-" + removed_id); + check(output && output->text.find("Removed node") != std::string::npos, + "Review lists exact removed identity and its last-good name"); + check(store.current_manifest(asset_id).at("generation") == initial_generation, + "Showing a conflict must not accept the removal"); + renderer.render(ui.snapshot()); + renderer.capture(root / "import-removal-review.ppm"); + gltf["nodes"][0]["translation"] = {1, 0, 0}; + atomic_write_json(source, gltf); + before = jobs(session); + click(ui, group + "-accept"); + const auto stale_id = new_job(session, before); + const auto stale = wait(session, ui, stale_id); + check(stale.at("state") == "conflict" && + store.current_manifest(asset_id).at("generation") == initial_generation, + "Changed source refuses reviewed acceptance and keeps the old generation: " + + stale.dump()); + click(ui, "tab-conflicts"); + const auto stale_group = "import-conflict-" + stale_id; + check(ui.widgets().find(stale_group + "-diagnostic")->text.find("changed since review") != + std::string::npos, + "Stale acceptance exposes the changed-generation diagnostic and new review"); + before = jobs(session); + click(ui, stale_group + "-retry"); + const auto fresh_id = new_job(session, before); + const auto fresh = wait(session, ui, fresh_id); + check(fresh.at("state") == "conflict", "Reimport creates a fresh removal review"); + click(ui, "tab-jobs"); + click(ui, "job-review-" + fresh_id); + check(!ui.widgets().find(group), "New conflict supersedes the explicitly retried review"); + before = jobs(session); + click(ui, "import-conflict-" + fresh_id + "-accept"); + const auto accepted_id = new_job(session, before); + const auto accepted = wait(session, ui, accepted_id); + check(accepted.at("state") == "succeeded", "Explicit reviewed removal must publish"); + check(store.current_manifest(asset_id).at("generation") == fresh["result"]["generation"], + "UI publishes exactly the reviewed candidate"); + check(store.current_manifest(asset_id).at("settings").at("units") == "metres", + "Retry preserves the saved import settings"); + click(ui, "tab-conflicts"); + check(ui.widgets().find("conflicts-empty"), + "Resolved import conflicts leave the review list"); + check(session.authoring().query(ui.current_document()) == document_before, + "Import approval never rewrites the authoring scene or its Undo revision"); + renderer.render(ui.snapshot()); + check(renderer.stats().validation_errors == 0, "Vulkan validation"); + std::cout << "Import UI: removed IDs/names, stale review rejection, explicit retry and " + "exact-generation acceptance passed. " + << path_to_utf8(root) << '\n'; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << "\nRetained: " << path_to_utf8(root) << '\n'; + return 1; + } +} diff --git a/tests/editor_ui_launcher.cpp b/tests/editor_ui_launcher.cpp index 5816895..aaaa971 100644 --- a/tests/editor_ui_launcher.cpp +++ b/tests/editor_ui_launcher.cpp @@ -1,8 +1,45 @@ +#include #include #include #include using namespace faset; namespace { +class ScopedPathEnvironment { + public: +#ifdef _WIN32 + ScopedPathEnvironment(const wchar_t* name, const std::filesystem::path& value) : name_(name) { + if (const auto* previous = _wgetenv(name)) + old_ = previous; + if (_wputenv_s(name, value.c_str()) != 0) + throw std::runtime_error("Cannot set test environment"); + } + ~ScopedPathEnvironment() { + _wputenv_s(name_.c_str(), old_.c_str()); + } + + private: + std::wstring name_, old_; +#else + ScopedPathEnvironment(const char* name, const std::filesystem::path& value) : name_(name) { + if (const auto* previous = std::getenv(name)) { + old_ = previous; + existed_ = true; + } + if (setenv(name, value.c_str(), 1) != 0) + throw std::runtime_error("Cannot set test environment"); + } + ~ScopedPathEnvironment() { + if (existed_) + setenv(name_.c_str(), old_.c_str(), 1); + else + unsetenv(name_.c_str()); + } + + private: + std::string name_, old_; + bool existed_ = false; +#endif +}; void check(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); @@ -41,19 +78,23 @@ void text(editor::ProjectLauncher& launcher, const std::string& id, const std::s } } // namespace int main() { - const auto root = std::filesystem::temp_directory_path() / ("faset-launcher-ui-" + new_id()); + const auto root = + std::filesystem::temp_directory_path() / path_from_utf8("faset-проекты-" + new_id()); try { - const auto existing = root / "Existing project"; + const auto existing = root / path_from_utf8("Существующий проект"); + const auto new_project = root / path_from_utf8("Новая игра"); atomic_write_json(existing / "project.faset.json", {{"format", "faset.project"}, {"version", 1}, {"name", "Existing project"}, {"dimension", 2}}); const auto recents = root / "recent-projects.json"; - atomic_write_json(recents, Json::array({existing.string(), (root / "Missing").string(), - existing.string()})); + atomic_write_json(recents, + Json::array({path_to_utf8(existing), path_to_utf8(root / "Missing"), + path_to_utf8(existing)})); render::Renderer renderer({1100, 720, "Project launcher acceptance", true, true}); { - editor::ProjectLauncher launcher(renderer, FASET_TEST_ENGINE, {}, recents); + editor::ProjectLauncher launcher(renderer, path_from_utf8(FASET_TEST_ENGINE), {}, + recents); launcher.frame({}); check(launcher.widgets().find("launcher-create")->selected, "No initial path starts Create"); @@ -61,11 +102,11 @@ int main() { !launcher.widgets().find("launcher-recent-1"), "Recent list includes real valid unique projects only"); text(launcher, "launcher-name", "Тестовый проект"); - text(launcher, "launcher-path", existing.string()); + text(launcher, "launcher-path", path_to_utf8(existing)); click(launcher, "launcher-submit"); check(!launcher.selection() && !launcher.widgets().find("launcher-error")->text.empty(), "Create must reject nonempty existing project"); - text(launcher, "launcher-path", (root / "New Game").string()); + text(launcher, "launcher-path", path_to_utf8(new_project)); click(launcher, "launcher-2d"); check(launcher.widgets().find("launcher-2d")->selected, "2D project selection"); renderer.render(launcher.snapshot()); @@ -73,35 +114,36 @@ int main() { launcher.frame({key("Return", true)}); check(launcher.selection() && launcher.selection()->create && launcher.selection()->dimension == 2 && - launcher.selection()->name == "Тестовый проект", + launcher.selection()->name == "Тестовый проект" && + launcher.selection()->path == new_project, "Create through keyboard returns typed selection"); - check(!std::filesystem::exists(root / "New Game"), + check(!std::filesystem::exists(new_project), "Launcher does not create a partial project before Session scaffold"); } { - editor::ProjectLauncher launcher(renderer, FASET_TEST_ENGINE, root / "Missing", - recents); + editor::ProjectLauncher launcher(renderer, path_from_utf8(FASET_TEST_ENGINE), + root / "Missing", recents); launcher.frame({}); click(launcher, "launcher-submit"); check(!launcher.selection() && !launcher.widgets().find("launcher-error")->text.empty(), "Open validates missing directory"); click(launcher, "launcher-recent-0"); - check(launcher.widgets().find("launcher-path")->text == existing.string(), + check(launcher.widgets().find("launcher-path")->text == path_to_utf8(existing), "Recent selection fills actual path"); click(launcher, "launcher-browse"); check(launcher.widgets().find("launcher-browser")->visible, "Native retained directory browser opens"); - text(launcher, "browser-path", (root / "Absent folder").string()); + text(launcher, "browser-path", path_to_utf8(root / "Absent folder")); launcher.frame({key("Return", true)}); check(launcher.widgets().find("launcher-browser")->visible && !launcher.widgets().find("browser-choose")->enabled, "Invalid typed directory cannot silently select prior directory"); - text(launcher, "browser-path", existing.string()); + text(launcher, "browser-path", path_to_utf8(existing)); click(launcher, "browser-up"); - check(launcher.widgets().find("browser-path")->text == root.string(), + check(launcher.widgets().find("browser-path")->text == path_to_utf8(root), "Folder browser Up navigation"); click(launcher, "browser-entry-0"); - check(launcher.widgets().find("browser-path")->text == existing.string(), + check(launcher.widgets().find("browser-path")->text == path_to_utf8(existing), "Directory list navigation"); renderer.render(launcher.snapshot()); renderer.capture(root / "launcher-browser.ppm"); @@ -114,11 +156,56 @@ int main() { launcher.selection()->dimension == 2, "Open reads project metadata"); } + { + // Exercise the path returned by Create after the caller has created its + // project, then reopen it through the launcher's UTF-8 text boundary. + atomic_write_json(new_project / "project.faset.json", {{"format", "faset.project"}, + {"version", 1}, + {"name", "Тестовый проект"}, + {"dimension", 2}}); + editor::ProjectLauncher launcher(renderer, path_from_utf8(FASET_TEST_ENGINE), {}, + recents); + launcher.frame({}); + click(launcher, "launcher-open"); + text(launcher, "launcher-path", path_to_utf8(new_project / "project.faset.json")); + launcher.frame({key("Return", true)}); + check(launcher.selection() && launcher.selection()->path == new_project && + launcher.selection()->name == "Тестовый проект", + "Unicode project directory survives typed manifest path and reopen"); + } + { + const auto home = root / path_from_utf8("Дом пользователя"); + const auto config = root / path_from_utf8("Настройки пользователя"); + std::filesystem::create_directories(home); +#ifdef _WIN32 + ScopedPathEnvironment user_home(L"USERPROFILE", home), app_config(L"APPDATA", config); + const auto recent_file = config / "Faset/recent-projects.json"; +#else + ScopedPathEnvironment user_home("HOME", home), app_config("XDG_CONFIG_HOME", config); + const auto recent_file = config / "faset/recent-projects.json"; +#endif + editor::remember_project(existing); + check(read_json(recent_file).at(0) == path_to_utf8(existing), + "Unicode config directory and recent project are serialized as UTF-8"); + editor::ProjectLauncher launcher(renderer, path_from_utf8(FASET_TEST_ENGINE)); + launcher.frame({}); + check(launcher.widgets().find("launcher-path")->text == + path_to_utf8(home / "FasetProjects/MyGame"), + "Unicode user home forms a native default project path"); + check(launcher.widgets().find("launcher-recent-0"), + "Default recents path reads the Unicode config directory"); + click(launcher, "launcher-browse"); + click(launcher, "browser-home"); + check(launcher.widgets().find("browser-path")->text == path_to_utf8(home), + "Home navigation preserves Unicode environment path"); + launcher.frame({key("Escape")}); + } { const auto corrupt = root / "Corrupt"; atomic_write_json(corrupt / "project.faset.json", {{"format", "faset.project"}, {"version", 9}, {"name", "Future"}}); - editor::ProjectLauncher launcher(renderer, FASET_TEST_ENGINE, corrupt, recents); + editor::ProjectLauncher launcher(renderer, path_from_utf8(FASET_TEST_ENGINE), corrupt, + recents); launcher.frame({}); click(launcher, "launcher-submit"); check(!launcher.selection() && !launcher.widgets().find("launcher-error")->text.empty(), @@ -129,10 +216,10 @@ int main() { check(renderer.stats().validation_errors == 0, "Launcher Vulkan validation"); std::cout << "Launcher Unicode/create/open/validation/recents/directory browser/keyboard passed. " - << root << '\n'; + << path_to_utf8(root) << '\n'; return 0; } catch (const std::exception& e) { - std::cerr << e.what() << "\nRetained: " << root << '\n'; + std::cerr << e.what() << "\nRetained: " << path_to_utf8(root) << '\n'; return 1; } } diff --git a/tests/editor_ui_project_settings.cpp b/tests/editor_ui_project_settings.cpp index bc1c6d9..55c9335 100644 --- a/tests/editor_ui_project_settings.cpp +++ b/tests/editor_ui_project_settings.cpp @@ -51,16 +51,17 @@ int main() { {"name", "Original project"}, {"dimension", 3}, {"custom_metadata", {{"preserve", true}}}}); - editor::Session session({root, FASET_TEST_ENGINE, root}); + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); render::Renderer renderer({1280, 800, "Project settings acceptance", true, true}); editor::EditorUI ui(session, renderer, - std::filesystem::path(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", - std::filesystem::path(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); ui.frame({}); click(ui, "add-cube"); - session.authoring().save(ui.current_document(), "Scenes/Main.scene.json"); + session.authoring().save(ui.current_document(), + path_from_utf8("Scenes/Главная.scene.json")); const auto other = session.authoring().create("Other", 2); - session.authoring().save(other.at("id"), "Scenes/Other.scene.json"); + session.authoring().save(other.at("id"), path_from_utf8("Scenes/Другая.scene.json")); ui.frame({}); const auto document_before = session.authoring().query(ui.current_document()); const auto project_before = read_json(project_file); @@ -75,12 +76,12 @@ int main() { text(ui, "project-settings-name", "Новый проект"); click(ui, "project-settings-2d"); click(ui, "project-scene-choice-1"); - check(ui.widgets().find("project-settings-start")->text == "Scenes/Other.scene.json", + check(ui.widgets().find("project-settings-start")->text == "Scenes/Другая.scene.json", "Saved scene chooser sets project-relative path"); click(ui, "project-settings-save"); auto saved = read_json(project_file); check(saved["name"] == "Новый проект" && saved["dimension"] == 2 && - saved["start_scene"] == "Scenes/Other.scene.json", + saved["start_scene"] == "Scenes/Другая.scene.json", "Explicit Save project persists typed settings"); check(saved["custom_metadata"] == project_before["custom_metadata"] && saved["id"] == project_before["id"], @@ -113,7 +114,7 @@ int main() { click(ui, "project-settings-3d"); click(ui, "project-scene-choice-0"); click(ui, "project-settings-save"); - check(read_json(project_file)["start_scene"] == "Scenes/Main.scene.json", + check(read_json(project_file)["start_scene"] == "Scenes/Главная.scene.json", "Save after explicit reload uses new content revision"); check(session.authoring().query(ui.current_document()) == document_before, "Project settings remain outside scene Undo throughout conflicts"); @@ -126,10 +127,10 @@ int main() { check(renderer.stats().validation_errors == 0, "Project settings Vulkan validation"); std::cout << "Project settings name/type/start-scene Save/Cancel/Reload/conflict, metadata " "preservation and separate scene Undo passed. " - << root << '\n'; + << path_to_utf8(root) << '\n'; return 0; } catch (const std::exception& e) { - std::cerr << e.what() << "\nRetained: " << root << '\n'; + std::cerr << e.what() << "\nRetained: " << path_to_utf8(root) << '\n'; return 1; } } diff --git a/tests/editor_ui_reload.cpp b/tests/editor_ui_reload.cpp index a982269..dbd111f 100644 --- a/tests/editor_ui_reload.cpp +++ b/tests/editor_ui_reload.cpp @@ -39,15 +39,14 @@ int main() { try { const auto styles = root / "styles/dark.json", layout_path = root / "styles/editor-layout.json"; - auto theme = read_json(std::filesystem::path(FASET_TEST_ENGINE) / "assets/ui/dark.json"); - auto layout = - read_json(std::filesystem::path(FASET_TEST_ENGINE) / "assets/ui/editor-layout.json"); + auto theme = read_json(path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + auto layout = read_json(path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/editor-layout.json"); atomic_write_json(styles, theme); atomic_write_json(layout_path, layout); - editor::Session session({root / "project", FASET_TEST_ENGINE, root}); + editor::Session session({root / "project", path_from_utf8(FASET_TEST_ENGINE), root}); render::Renderer renderer({1280, 800, "Style reload acceptance", true, true}); editor::EditorUI ui(session, renderer, - std::filesystem::path(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", styles); ui.frame({}); click(ui, "add-cube"); @@ -118,10 +117,10 @@ int main() { check(renderer.stats().validation_errors == 0, "Reload Vulkan validation"); std::cout << "Theme/layout live reload pixels+geometry, atomic rejection, focus+draft " "preservation, callback and diagnostic dedup passed. " - << root << '\n'; + << path_to_utf8(root) << '\n'; return 0; } catch (const std::exception& e) { - std::cerr << e.what() << "\nRetained: " << root << '\n'; + std::cerr << e.what() << "\nRetained: " << path_to_utf8(root) << '\n'; return 1; } } diff --git a/tests/editor_ui_templates.cpp b/tests/editor_ui_templates.cpp index 8fcc8b5..43196bb 100644 --- a/tests/editor_ui_templates.cpp +++ b/tests/editor_ui_templates.cpp @@ -73,20 +73,20 @@ float position(const Json& object) { int main() { const auto root = std::filesystem::temp_directory_path() / ("faset-template-ui-" + new_id()); try { - editor::Session session({root, FASET_TEST_ENGINE, root}); + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); render::Renderer renderer({1280, 900, "Template workflow test", true, true}); editor::EditorUI ui(session, renderer, - std::filesystem::path(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", - std::filesystem::path(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); ui.frame({}); const auto main = ui.current_document(); click(ui, "add-cube"); text(ui, "object-name", "Door"); const auto local = ui.selected_entity(); click(ui, "menu-Scene"); - text(ui, "template-path", "Assets/Templates/Door.scene.json"); + text(ui, "template-path", "Assets/Templates/Дверь.scene.json"); click(ui, "save-template"); - check(std::filesystem::exists(root / "Assets/Templates/Door.scene.json"), + check(std::filesystem::exists(root / path_from_utf8("Assets/Templates/Дверь.scene.json")), "Manual source template creation"); for (int i = 0; i < 2; ++i) { click(ui, "menu-Scene"); @@ -105,12 +105,12 @@ int main() { click(ui, "menu-Scene"); click(ui, "instance-template"); click(ui, "menu-File"); - text(ui, "save-path", "Assets/Templates/Outer.scene.json"); + text(ui, "save-path", "Assets/Templates/Внешняя.scene.json"); click(ui, "save-as-button"); click(ui, "back-document"); check(ui.current_document() == main, "Open source/back document navigation"); click(ui, "menu-Scene"); - text(ui, "template-path", "Assets/Templates/Outer.scene.json"); + text(ui, "template-path", "Assets/Templates/Внешняя.scene.json"); click(ui, "instance-template"); auto nested = leaf(session, main, 2); const auto nested_id = nested.at("id").get(); @@ -178,6 +178,35 @@ int main() { check(query(session, ui)["scene"]["simulation"]["gravity"][1] == -9.81, "Simulation Undo restores gravity"); + // Save through the same controls a user sees, then open a new editor + // session: an in-memory resolver alone cannot prove persistent identity. + click(ui, "entity-" + nested_id); + click(ui, "object-open-source"); + click(ui, "save"); + click(ui, "back-document"); + click(ui, "menu-File"); + text(ui, "save-path", "Scenes/Экземпляры.scene.json"); + click(ui, "save-as-button"); + const auto saved_scene = query(session, ui).at("scene"); + const auto saved_resolved = session.commands().resolved_scene(main); + { + editor::Session reopened({root, path_from_utf8(FASET_TEST_ENGINE), root}); + editor::EditorUI reopened_ui( + reopened, renderer, path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + reopened_ui.frame({}); + click(reopened_ui, "menu-File"); + text(reopened_ui, "open-path", "Scenes/Экземпляры.scene.json"); + click(reopened_ui, "open-path-button"); + check(query(reopened, reopened_ui).at("scene") == saved_scene, + "Save and reopen preserve instance IDs, overrides and local additions"); + check(reopened.commands().resolved_scene(reopened_ui.current_document()) == + saved_resolved, + "New session resolves saved nested source edits with identical provenance"); + check(leaf(reopened, reopened_ui.current_document(), 2).at("id") == nested_id, + "Resolved nested object identity survives reopening"); + } + nested = leaf(session, main, 2); click(ui, "entity-" + nested_id); text(ui, position_field(nested), "7"); @@ -218,10 +247,10 @@ int main() { std::cout << "Manual templates: create/two instances/nesting/overrides/Revert/source " "rename/suppression restore/local additions/conflicts/opaque versions; " "simulation UI+Undo passed. " - << root << '\n'; + << path_to_utf8(root) << '\n'; return 0; } catch (const std::exception& error) { - std::cerr << error.what() << "\nRetained: " << root << '\n'; + std::cerr << error.what() << "\nRetained: " << path_to_utf8(root) << '\n'; return 1; } } diff --git a/tests/editor_ui_tests.cpp b/tests/editor_ui_tests.cpp index fa10671..2a254d8 100644 --- a/tests/editor_ui_tests.cpp +++ b/tests/editor_ui_tests.cpp @@ -44,7 +44,8 @@ void text(editor::EditorUI& ui, const std::string& id, const std::string& value, ui.frame(events); } int main() { - auto root = std::filesystem::temp_directory_path() / ("faset-ui-authoring-" + new_id()); + auto root = + std::filesystem::temp_directory_path() / path_from_utf8("faset-ui-проект-" + new_id()); try { std::filesystem::create_directories(root); atomic_write_json(root / "project.faset.json", {{"format", "faset.project"}, @@ -53,15 +54,16 @@ int main() { {"dimension", 3}}); #if defined(FASET_TEST_PLUGIN_DIRECTORY) std::filesystem::create_directories(root / "Plugins"); - for (const auto& file : std::filesystem::directory_iterator(FASET_TEST_PLUGIN_DIRECTORY)) + for (const auto& file : + std::filesystem::directory_iterator(path_from_utf8(FASET_TEST_PLUGIN_DIRECTORY))) if (file.is_regular_file()) std::filesystem::copy_file(file.path(), root / "Plugins" / file.path().filename()); #endif - editor::Session session({root, FASET_TEST_ENGINE, root}); + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); render::Renderer renderer({1280, 800, "Faset editor test", true, true}); editor::EditorUI ui(session, renderer, - std::filesystem::path(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", - std::filesystem::path(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); ui.frame({}); click(ui, "add-cube"); auto state = session.authoring().query(ui.current_document()); @@ -179,7 +181,7 @@ int main() { click(ui, "tab-assets"); #endif std::filesystem::create_directories(root / "Assets"); - const auto image_path = root / "Assets/TwoPixels.png"; + const auto image_path = root / path_from_utf8("Assets/Два пикселя.png"); { std::ofstream stream(image_path, std::ios::binary); const auto& png = faset::test_images::png_red_green; @@ -188,8 +190,8 @@ int main() { } const auto import_job = session.commands() - .call("faset_import", - {{"path", "Assets/TwoPixels.png"}, {"settings", {{"pixels_per_unit", 1.0}}}}) + .call("faset_import", {{"path", "Assets/Два пикселя.png"}, + {"settings", {{"pixels_per_unit", 1.0}}}}) .at("job") .get(); Json imported; @@ -206,6 +208,8 @@ int main() { const auto image_id = imported.at("result").at("asset_id").get(); auto* image_row = ui.widgets().find("asset-" + image_id); check(image_row, "Imported image in asset browser"); + check(image_row->text.find("Два пикселя") != std::string::npos, + "Asset browser displays Unicode source filename"); const auto asset_rect = image_row->rect.intersection(image_row->clip); const auto viewport_rect = ui.widgets().find("viewport")->rect; down.x = asset_rect.x + 100; @@ -253,7 +257,7 @@ int main() { std::cout << "Editor UI: actual events create/select/rename/typed " "fields/Undo/Redo/conflict/one drag transaction passed. " "Screenshot: " - << (root / "editor-ui.ppm") << '\n'; + << path_to_utf8(root / "editor-ui.ppm") << '\n'; return 0; } catch (const std::exception& e) { std::cerr << e.what() << '\n'; diff --git a/tests/mcp_stdio_test.py b/tests/mcp_stdio_test.py index 053d850..9268bab 100644 --- a/tests/mcp_stdio_test.py +++ b/tests/mcp_stdio_test.py @@ -44,8 +44,8 @@ def run(executable, project): names = {tool["name"] for tool in request("tools/list")["result"]["tools"]} assert {"faset_scene_edit", "faset_export", "faset_job_cancel", "faset_plugins"} <= names assert "faset_runtime_query" not in names and "faset_editor_capture" not in names - if (project / "Scenes/main.scene.json").exists(): - opened = call("faset_document_open", {"path": "Scenes/main.scene.json"}) + if (project / "Scenes/Начало Café 世界.scene.json").exists(): + opened = call("faset_document_open", {"path": "Scenes/Начало Café 世界.scene.json"}) assert opened["scene"]["entities"][0]["name"] == "Door 世界" return scene = call("faset_document_create", {"name": "MCP integration", "dimension": 3}) @@ -62,7 +62,7 @@ def run(executable, project): assert undone["scene"]["entities"] == [] redone = call("faset_redo", {"document": identity, "revision": 2}) assert redone["scene"] == changed["scene"] - saved = call("faset_document_save", {"document": identity, "path": "Scenes/main.scene.json"}) + saved = call("faset_document_save", {"document": identity, "path": "Scenes/Начало Café 世界.scene.json"}) assert not saved["dirty"] call("faset_document_open", {"path": "../outside.json"}, True) assert request("resources/read", {"uri": "faset://documents"})["result"]["contents"] @@ -109,8 +109,14 @@ def eof_tail(executable, project): with tempfile.TemporaryDirectory(prefix="faset-mcp-stdio-") as temporary: - run(sys.argv[1], pathlib.Path(temporary)) - run(sys.argv[1], pathlib.Path(temporary)) - disconnected_output(sys.argv[1], pathlib.Path(temporary)) - eof_tail(sys.argv[1], pathlib.Path(temporary)) + project = pathlib.Path(temporary) / "Faset Café 世界" + created = subprocess.run([sys.argv[1], "--project", str(project), "--new", "Проект Café 世界", + "--command", json.dumps({"name": "faset_project", "arguments": {}}, ensure_ascii=False)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert created.returncode == 0, created.stderr + assert json.loads((project / "project.faset.json").read_text(encoding="utf-8"))["name"] == "Проект Café 世界" + run(sys.argv[1], project) + run(sys.argv[1], project) + disconnected_output(sys.argv[1], project) + eof_tail(sys.argv[1], project) print("Real MCP stdio lifecycle, clean stdout, revision conflict, retry, Undo/Redo, disk reopen, broken output pipe and EOF shutdown passed") diff --git a/tests/player_diagnostics/Gameplay.cpp b/tests/player_diagnostics/Gameplay.cpp new file mode 100644 index 0000000..501a433 --- /dev/null +++ b/tests/player_diagnostics/Gameplay.cpp @@ -0,0 +1,16 @@ +#include "Gameplay.hpp" +#include + +namespace faset::gameplay { +void registerGameplay(runtime::Runtime& runtime) { + runtime::Behavior behavior; + behavior.onStart = [](auto&, auto, double) { throw std::runtime_error("TEST_ON_START"); }; + behavior.update = [](auto&, auto, double) { throw std::runtime_error("TEST_UPDATE"); }; + behavior.onDestroy = [](auto&, auto, double) { throw std::runtime_error("TEST_ON_DESTROY"); }; + runtime.registerBehavior("test.diagnostics", std::move(behavior)); +} +nlohmann::json schema() { + return nlohmann::json::array( + {{{"id", "test.diagnostics"}, {"version", 2}, {"fields", nlohmann::json::object()}}}); +} +} // namespace faset::gameplay diff --git a/tests/player_diagnostics/Gameplay.hpp b/tests/player_diagnostics/Gameplay.hpp new file mode 100644 index 0000000..536ab72 --- /dev/null +++ b/tests/player_diagnostics/Gameplay.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace faset::gameplay { +void registerGameplay(runtime::Runtime& runtime); +nlohmann::json schema(); +} // namespace faset::gameplay diff --git a/tests/player_diagnostics_test.py b/tests/player_diagnostics_test.py new file mode 100644 index 0000000..3227fef --- /dev/null +++ b/tests/player_diagnostics_test.py @@ -0,0 +1,39 @@ +"""Normal Player shutdown reports lifecycle failures without losing completed stats.""" +import json +from pathlib import Path +import subprocess +import sys +import tempfile + + +with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporary: + root = Path(temporary) / "Faset Café 世界" + root.mkdir() + scene = root / "Сцена.scene.json" + profile = root / "Профиль.json" + scene.write_text(json.dumps({ + "format": "faset.scene", "version": 1, "id": "diagnostics-scene", + "name": "Lifecycle diagnostics", "dimension": 2, "instances": [], + "entities": [{"id": "diagnostics-object", "name": "Throwing behavior", "parent": None, + "components": [{"id": "diagnostics-component", "type": "test.diagnostics", + "version": 2, "fields": {}}]}] + }), encoding="utf-8") + result = subprocess.run([sys.argv[1], "--scene", str(scene), "--headless", "--frames", "1", + "--profile", str(profile)], + capture_output=True, text=True, encoding="utf-8", timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + for phase in ["TEST_ON_START", "TEST_UPDATE", "TEST_ON_DESTROY"]: + assert result.stderr.count(phase) == 1, (phase, result.stderr) + summary = json.loads(result.stdout) + assert summary["frames"] == 1 and summary["ticks"] == 1, summary + report = json.loads(profile.read_text(encoding="utf-8")) + assert report["completed_frames"] == 1 and len(report["samples"]) == 1, report + assert report["samples"][0]["tick"] == 1, report["samples"] + + # The same linked v2 schema must validate without registering or invoking behavior. + validated = subprocess.run([sys.argv[1], "--scene", str(scene), "--validate"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert validated.returncode == 0, validated.stderr + assert "TEST_" not in validated.stderr, validated.stderr + assert json.loads(validated.stdout)["validated"] +print("Player normal shutdown diagnostics, preserved tick/profile stats and callback-free validation passed") diff --git a/tests/render_reload_tests.cpp b/tests/render_reload_tests.cpp index ca9523b..91471b6 100644 --- a/tests/render_reload_tests.cpp +++ b/tests/render_reload_tests.cpp @@ -14,11 +14,11 @@ void require(bool value, const char* message) { throw std::runtime_error(message); } int compile(const fs::path& source, const fs::path& output) { - Process process( - {{FASET_PYTHON_EXECUTABLE, FASET_SHADER_COMPILE_TOOL, "--compiler", FASET_TEST_SLANGC, - "--source", source.string(), "--entry", "fragmentMain", "--output", output.string()}, - {}, - {}}); + Process process({{FASET_PYTHON_EXECUTABLE, FASET_SHADER_COMPILE_TOOL, "--compiler", + FASET_TEST_SLANGC, "--source", path_to_utf8(source), "--entry", + "fragmentMain", "--output", path_to_utf8(output)}, + {}, + {}}); std::string diagnostics; while (true) { auto result = process.poll(); @@ -34,12 +34,13 @@ int compile(const fs::path& source, const fs::path& output) { } } // namespace int main() { - const auto temporary = fs::temp_directory_path() / ("faset-shader-reload-" + new_id()); + const auto temporary = + fs::temp_directory_path() / path_from_utf8("Faset shader Café 世界 " + new_id()); struct Cleanup { fs::path path; ~Cleanup() { std::error_code ignored; - fs::remove_all(path, ignored); + fs::remove_all(native_io_path(path), ignored); } } cleanup{temporary}; try { @@ -48,10 +49,10 @@ int main() { for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"}) for (const auto* extension : {".spv", ".reflection.json"}) { const auto name = std::string(entry) + extension; - fs::copy_file(fs::path(FASET_TEST_SHADER_DIRECTORY) / name, bundle / name); + fs::copy_file(path_from_utf8(FASET_TEST_SHADER_DIRECTORY) / name, bundle / name); } const auto source = temporary / "reload.slang"; - const auto original_source = read_text(FASET_TEST_SHADER_SOURCE); + const auto original_source = read_text(path_from_utf8(FASET_TEST_SHADER_SOURCE)); const auto original_spirv = read_text(bundle / "fragmentMain.spv"); const auto original_reflection = read_text(bundle / "fragmentMain.reflection.json"); const auto original_fingerprint = Json::parse(original_reflection).at("layout_fingerprint"); @@ -63,9 +64,39 @@ int main() { configuration.shader_directory = bundle; render::Renderer renderer(configuration); render::Snapshot scene; - scene.ui_quads.push_back({0, 0, 64, 64, {1, .8f, .4f, 1}}); + scene.ui_quads.push_back({0, 0, 32, 64, {1, .8f, .4f, 1}}); + scene.sprites.push_back({{.5f, 0, .5f}, {1, 2}, {.2f, 1, .4f, 1}}); renderer.render(scene); auto expected = renderer.pixels(); + // Cooked/exported generations can exceed MAX_PATH even for modest project + // names. Exercise the renderer's file boundaries without relying on the + // external shader compiler's own long-path policy. + const auto deep_bundle = temporary / std::string(96, 'a') / std::string(96, 'b') / + std::string(96, 'c') / "shaders"; + require(deep_bundle.native().size() > 300, + "Shader file fixture must exceed the legacy Windows path limit"); + fs::create_directories(native_io_path(deep_bundle)); + for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"}) + for (const auto* extension : {".spv", ".reflection.json"}) { + const auto name = std::string(entry) + extension; + atomic_write(deep_bundle / name, read_text(bundle / name)); + } + render::validate_shader_bundle(deep_bundle); + { + auto deep_configuration = configuration; + deep_configuration.shader_directory = deep_bundle; + render::Renderer deep_renderer(deep_configuration); + deep_renderer.render(scene); + require(deep_renderer.pixels() == expected, + "Deep Unicode shader bundle must render the same pixels"); + const auto capture = deep_bundle / path_from_utf8("Capture Café 世界.ppm"); + deep_renderer.capture(capture); + const auto bytes = read_text(capture); + require(bytes.starts_with("P6\n64 64\n255\n") && bytes.size() == 13 + 64 * 64 * 3, + "Deep Unicode capture must contain the complete rendered image"); + require(deep_renderer.stats().validation_errors == 0, + "Deep shader bundle has no Vulkan validation errors"); + } require(renderer.stats().gpu_allocated_bytes > 1024 * 1024 && renderer.stats().texture_count >= 1, "Frame profile reports live Vulkan allocations and textures"); diff --git a/tests/render_sprite_tests.cpp b/tests/render_sprite_tests.cpp new file mode 100644 index 0000000..777b0f4 --- /dev/null +++ b/tests/render_sprite_tests.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include + +using namespace faset::render; +namespace { +void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} +} // namespace +int main() { + try { + Renderer renderer({64, 64, "Faset sprite alpha validation", true, true}); + Snapshot scene; + scene.clear_color = {0, 0, 0, 1}; + Sprite front{{0, 0, .2f}, {2, 2}, {0, 1, 0, 0}}; + Sprite back{{0, 0, .8f}, {2, 2}, {1, 0, 0, 1}}; + auto color = [&](int red, int green, const char* message) { + renderer.render(scene); + const auto pixels = renderer.pixels(); + const auto index = (32 * 64 + 32) * 4; + require(std::abs(int(pixels[index]) - red) <= 2 && + std::abs(int(pixels[index + 1]) - green) <= 2 && pixels[index + 2] <= 2, + message); + require(renderer.stats().validation_errors == 0, "Sprite Vulkan validation error"); + }; + scene.sprites = {front, back}; + color(255, 0, "Fully transparent front sprite must not occlude a later opaque sprite"); + front.color[3] = .5f; + scene.sprites = {front, back}; + color(128, 128, "Partial alpha must composite over the farther sprite"); + scene.sprites = {back, front}; + color(128, 128, "Unequal-depth compositing must be independent of submission order"); + front.position[2] = .9f; + front.layer = 5; + scene.sprites = {front, back}; + color(128, 128, "Explicit higher layer must take priority over sprite depth"); + front.position[2] = back.position[2]; + front.layer = back.layer; + scene.sprites = {front, back}; + color(255, 0, "Equal-layer/equal-depth sprites must preserve submission order"); + scene.sprites = {back, front}; + color(128, 128, "Later equal-depth sprite must composite on top"); + auto texture = std::make_shared(); + texture->width = texture->height = 1; + texture->rgba = {0, 255, 0, 0}; + front.color = {1, 1, 1, 1}; + front.texture = texture; + front.position[2] = .2f; + scene.sprites = {front, back}; + color(255, 0, "Transparent texture pixels must preserve underlying sprites"); + texture->rgba[3] = 128; + ++texture->revision; + color(127, 128, "Texture alpha must blend with the already rendered background"); + auto opaque = std::make_shared(); + for (const auto point : {Vec3{-1, -1, .4f}, Vec3{1, -1, .4f}, Vec3{1, 1, .4f}, + Vec3{-1, -1, .4f}, Vec3{1, 1, .4f}, Vec3{-1, 1, .4f}}) + opaque->vertices.push_back({point, {0, 0, 0}, {1, 0, 0, 1}}); + scene.draws.push_back({opaque, identity, {1, 1, 1, 1}, .5f, 0, false}); + front.position[2] = .8f; + scene.sprites = {front}; + color(255, 0, "Sprites behind opaque mesh depth must remain occluded"); + front.position[2] = .2f; + scene.sprites = {front}; + color(127, 128, "Sprites in front of opaque mesh depth must composite normally"); + std::cout << "Sprite depth/layer ordering, stable ties, color/texture alpha and opaque " + "mesh occlusion passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/tests/render_window_tests.cpp b/tests/render_window_tests.cpp index 47da99c..e6d1dae 100644 --- a/tests/render_window_tests.cpp +++ b/tests/render_window_tests.cpp @@ -1,10 +1,13 @@ #include +#include #include +#include #include #include #include #include #include +#include #include namespace { @@ -66,6 +69,12 @@ int main(int argc, char** argv) { unsigned frame{}; auto draw = [&] { renderer.poll_events(); + const float native_scale = SDL_GetWindowDisplayScale(window); + require(std::isfinite(renderer.display_scale()) && renderer.display_scale() > 0.f, + "Renderer UI display scale must be finite and positive"); + if (std::isfinite(native_scale) && native_scale > 0.f) + require(std::abs(renderer.display_scale() - native_scale) < 0.001f, + "Renderer UI display scale must follow SDL's live window scale"); const unsigned channel = frame++ % 2; scene.ui_quads[0].color = channel ? Color{0, 1, 0, 1} : Color{1, 0, 0, 1}; renderer.render(scene); @@ -108,6 +117,61 @@ int main(int argc, char** argv) { draw(); SDL_Delay(10); } + { + // Do not replace an image/rich clipboard with its text projection. + // Plain text is restored without ever printing the saved contents. + std::size_t mime_count{}; + auto** mime_types = SDL_GetClipboardMimeTypes(&mime_count); + bool plain_text_only = true; + for (std::size_t i = 0; i < mime_count; ++i) { + const std::string type = mime_types[i]; + if (!type.starts_with("text/plain") && type != "UTF8_STRING" && type != "TEXT" && + type != "STRING" && type != "COMPOUND_TEXT" && type != "TARGETS" && + type != "TIMESTAMP" && type != "MULTIPLE") + plain_text_only = false; + } + SDL_free(mime_types); + if (plain_text_only) { + char* saved = SDL_GetClipboardText(); + require(saved != nullptr, "Read clipboard before reversible fixture"); + struct ClipboardRestore { + std::string saved; + bool active{true}; + ~ClipboardRestore() { + if (active) + SDL_SetClipboardText(saved.c_str()); + } + } restore{saved}; + SDL_free(saved); + const std::string fixture = "Faset clipboard Café 世界"; + renderer.set_clipboard(fixture); + require(renderer.clipboard() == fixture, "Native Unicode clipboard roundtrip"); + require(SDL_SetClipboardText(restore.saved.c_str()), "Restore saved clipboard"); + restore.active = false; + std::cout << "clipboard=unicode_roundtrip_passed\n"; + } else { + std::cout << "clipboard=skipped_to_preserve_non_text_payload\n"; + } + } + renderer.set_text_input(true); + require(SDL_TextInputActive(window), "Native SDL text input is active"); + renderer.set_text_input_area(31.5f, 22.5f, 160.5f, 24.5f); + int logical_width{}, logical_height{}, pixel_width{}, pixel_height{}, cursor{}; + SDL_GetWindowSize(window, &logical_width, &logical_height); + SDL_GetWindowSizeInPixels(window, &pixel_width, &pixel_height); + require(logical_width > 0 && logical_height > 0 && pixel_width > 0 && pixel_height > 0, + "Text input area has usable native window dimensions"); + SDL_Rect native_area{}; + require(SDL_GetTextInputArea(window, &native_area, &cursor), "Read native text input area"); + const float sx = float(logical_width) / pixel_width, + sy = float(logical_height) / pixel_height; + require(native_area.x == int(31.5f * sx) && native_area.y == int(22.5f * sy) && + native_area.w == std::max(1, int(160.5f * sx)) && + native_area.h == std::max(1, int(24.5f * sy)) && cursor == 0, + "Drawable-pixel input area must reach SDL in logical window units"); + renderer.set_text_input(false); + require(!SDL_TextInputActive(window), "Native SDL text input stops"); + std::cout << "text_input_area=coordinate_conversion_passed\n"; require(SDL_MinimizeWindow(window), "Request minimizing the owned fixture window"); if (!await([&] { return SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED; }, 2000)) { std::cout << "SKIP: window system declined the minimize request\n"; diff --git a/tests/runtime_tests.cpp b/tests/runtime_tests.cpp index 5618e76..6ab1fd2 100644 --- a/tests/runtime_tests.cpp +++ b/tests/runtime_tests.cpp @@ -1,6 +1,7 @@ #include "Gameplay.hpp" #include #include +#include #include #include #include @@ -174,6 +175,107 @@ void clockAndValidation() { auto schema = faset::gameplay::schema(); check(schema.size() == 2, "sample has explicit metadata without world"); } +void componentSchemaVersions() { + auto doc = scene(); + auto object = entity("versioned"); + auto custom = + component("test.versioned", {{"value", 42}, {"future_payload", {"kept", "opaque"}}}); + custom["version"] = 2; + object["components"].push_back(custom); + auto data = component("test.data", {{"description", "no behavior required"}}); + data["version"] = 3; + object["components"].push_back(data); + doc["entities"].push_back(object); + const auto source_before = doc; + const auto metadata = + Json::array({{{"id", "test.versioned"}, {"version", 2}, {"fields", Json::object()}}, + {{"id", "test.data"}, {"version", 3}, {"fields", Json::object()}}}); + validate_scene_schemas(doc, metadata); + validate_scene_schemas(doc, Json{{"types", metadata}}); + check(doc == source_before, "schema validation must not migrate or mutate source"); + int starts = 0; + Runtime world; + Behavior versioned; + versioned.onStart = [&](Runtime& runtime, EntityHandle handle, double) { + ++starts; + check(runtime.fields(handle, "test.versioned") == custom["fields"], + "versioned behavior receives exact opaque fields"); + }; + world.registerBehavior("test.versioned", versioned); + validate_scene_schemas(doc, metadata); + check(starts == 0, "schema validation does not invoke gameplay"); + world.load(doc); + const auto handle = world.find("versioned"); + check(starts == 1 && world.fields(handle, "test.data") == data["fields"], + "Runtime accepts positive custom versions including data-only components"); + auto added = component("test.added", {{"value", "deferred"}}); + added["version"] = 4; + world.addComponent(handle, added); + world.singleStep(); + check(world.fields(handle, "test.added") == added["fields"], + "deferred custom component accepts its positive version"); + for (const auto& invalid_version : Json::array({0, -1, 1.5, "2", true, nullptr})) { + auto invalid = doc; + invalid["entities"][0]["components"][1]["version"] = invalid_version; + rejects([&] { world.load(invalid); }, "custom version must be a positive integer"); + rejects([&] { validate_scene_schemas(invalid, metadata); }, + "schema gate rejects malformed component version"); + check(world.valid(handle), "invalid version preserves running world"); + } + for (const std::string builtin : + {"faset.transform", "faset.sprite", "faset.mesh", "faset.camera", "faset.light", + "faset.rigid_body_2d", "faset.rigid_body_3d"}) { + auto invalid = scene(); + auto native = component(builtin); + native["version"] = 2; + invalid["entities"].push_back({{"id", "native"}, {"components", Json::array({native})}}); + rejects([&] { world.load(invalid); }, + "all builtin components retain strict native version 1"); + rejects([&] { validate_scene_schemas(invalid, metadata); }, + "schema gate rejects builtin v2"); + check(world.valid(handle), "invalid builtin load preserves running world"); + } + auto bad_add = component("faset.camera"); + bad_add["version"] = 2; + world.addComponent(handle, bad_add); + world.singleStep(); + rejects([&] { world.fields(handle, "faset.camera"); }, + "invalid deferred builtin version cannot enter the world"); + rejects([&] { validate_scene_schemas(doc, Json::array()); }, + "Player schema gate rejects missing gameplay module metadata"); + auto mismatched = metadata; + mismatched[0]["version"] = 1; + rejects([&] { validate_scene_schemas(doc, mismatched); }, + "Player schema gate rejects gameplay version mismatch"); + auto unknown = doc; + unknown["entities"][0]["components"][1]["type"] = "test.missing"; + unknown["entities"][0]["components"][1]["version"] = 1; + rejects([&] { validate_scene_schemas(unknown, metadata); }, + "Unknown custom v1 must not silently run in Player"); + auto duplicate = metadata; + duplicate.push_back(metadata[0]); + rejects([&] { validate_scene_schemas(doc, duplicate); }, + "Ambiguous duplicate metadata TypeIds rejected"); + for (int version : {1, 2}) { + auto overridden_native = metadata; + overridden_native.push_back({{"id", "faset.transform"}, {"version", version}}); + rejects([&] { validate_scene_schemas(doc, overridden_native); }, + "Gameplay cannot redeclare native TypeIds, even at native version 1"); + } + auto invalid_schema = metadata; + invalid_schema[0]["version"] = "2"; + rejects([&] { validate_scene_schemas(doc, invalid_schema); }, + "Schema versions also require positive integers"); + rejects([&] { validate_scene_schemas(doc, Json::object()); }, + "Malformed schema manifest rejected"); + auto legacy = scene(); + legacy["entities"].push_back(entity("legacy")); + legacy["entities"][0]["components"][0].erase("version"); + validate_scene_schemas(legacy, Json::array()); + world.load(legacy); + check(bool(world.find("legacy")), + "Omitted component version retains development scene v1 compatibility"); +} void structuralFailuresAndCallbacks() { Runtime world; auto doc = scene(); @@ -315,6 +417,7 @@ int main() { run("Box3D", [] { physics(3); }); run("Lifecycle", lifecycle); run("Clock/validation", clockAndValidation); + run("Component schema versions", componentSchemaVersions); run("Structural failures", structuralFailuresAndCallbacks); run("Sample gameplay", sampleGameplay); run("Grounded jump", groundedJump); diff --git a/tests/ui_render_test.cpp b/tests/ui_render_test.cpp index 69a4900..8e142c7 100644 --- a/tests/ui_render_test.cpp +++ b/tests/ui_render_test.cpp @@ -2,13 +2,13 @@ #include #include using namespace faset; -int main(int argc, char** argv) { +int ui_render_main(int argc, char** argv) { try { render::Renderer renderer({1280, 800, "Faset UI reference implementation", true, true}); - ui::Context ui(FASET_TEST_FONT); - ui.set_theme(ui::Theme::load(FASET_UI_THEME)); + ui::Context ui(path_from_utf8(FASET_TEST_FONT)); + ui.set_theme(ui::Theme::load(path_from_utf8(FASET_UI_THEME))); ui.apply_layout( - read_json(std::filesystem::path(FASET_UI_THEME).parent_path() / "editor-layout.json")); + read_json(path_from_utf8(FASET_UI_THEME).parent_path() / "editor-layout.json")); auto& menu = ui.find("menubar")->add(ui::Kind::Row, "menuitems"); for (const auto& name : {"Faset", "File", "Edit", "Scene", "View", "Help"}) menu.add(ui::Kind::Button, "menu-" + std::string(name), name).layout.width = 65; @@ -102,7 +102,7 @@ int main(int argc, char** argv) { snapshot.draws.push_back(crate); ui.draw(snapshot); renderer.render(snapshot); - renderer.capture(argc > 1 ? argv[1] : "ui-test.ppm"); + renderer.capture(path_from_utf8(argc > 1 ? argv[1] : "ui-test.ppm")); if (renderer.stats().validation_errors) throw std::runtime_error("Vulkan validation reported UI rendering errors"); std::cout << "UI glyph atlas and retained panels rendered on " << renderer.stats().device @@ -113,3 +113,13 @@ int main(int argc, char** argv) { return 1; } } + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return run_utf8_main(argc, argv, ui_render_main); +} +#else +int main(int argc, char** argv) { + return ui_render_main(argc, argv); +} +#endif diff --git a/tests/ui_tests.cpp b/tests/ui_tests.cpp index 30cfcca..7dea7d2 100644 --- a/tests/ui_tests.cpp +++ b/tests/ui_tests.cpp @@ -1,4 +1,7 @@ +#include #include +#include +#include #include #include #include @@ -35,9 +38,85 @@ void click(ui::Context& context, const ui::Widget& widget) { context.handle(mouse(render::Event::Type::MouseDown, r.x + 5, r.y + 5)); context.handle(mouse(render::Event::Type::MouseUp, r.x + 5, r.y + 5)); } +void display_scale_contract() { + ui::Context context(path_from_utf8(FASET_TEST_FONT)); + auto& field = context.root().add(ui::Kind::TextField, "dpi-field", "Ж"); + field.layout.width = 180; + field.layout.height = 32; + auto& number = context.root().add(ui::Kind::NumberField, "dpi-number"); + number.layout.height = 32; + number.value = 4; + number.step = 1; + auto& button = context.root().add(ui::Kind::Button, "dpi-action", "Open"); + button.layout.width = 180; + button.layout.height = 32; + int clicks = 0, commits = 0, number_commits = 0; + button.on_click = [&](ui::Widget&) { ++clicks; }; + field.on_commit = [&](ui::Widget&) { ++commits; }; + number.on_commit = [&](ui::Widget&) { ++number_commits; }; + ui::Rect ime_area; + context.set_ime([](bool) {}, [&](ui::Rect rect) { ime_area = rect; }); + context.layout(320, 240, 1); + const auto initial = field.rect; + render::Snapshot one; + context.draw(one); + const auto first_glyph = std::find_if(one.ui_quads.begin(), one.ui_quads.end(), + [](const auto& q) { return bool(q.texture); }); + check(first_glyph != one.ui_quads.end(), "1x rasterized text missing"); + const auto glyph_width = first_glyph->width, glyph_height = first_glyph->height; + const auto revision = context.font().texture()->revision; + check(context.focus("dpi-field"), "DPI field focus"); + context.handle(key("A", true)); + context.handle(text("Живая сцена")); + context.layout(640, 480, 2); + check(field.rect.width == initial.width * 2 && field.rect.height == initial.height * 2 && + field.clip.width == initial.width * 2 && ime_area.width == field.rect.width, + "2x layout, clipping and IME rectangle use drawable pixels"); + check(context.focused_id() == "dpi-field" && field.text == "Живая сцена" && commits == 0, + "Live display scale preserves focused edit without committing"); + context.handle(key("Escape")); + render::Snapshot two; + context.draw(two); + const auto second_glyph = std::find_if(two.ui_quads.begin(), two.ui_quads.end(), + [](const auto& q) { return bool(q.texture); }); + check(second_glyph != two.ui_quads.end() && second_glyph->width >= glyph_width * 1.8f && + second_glyph->height >= glyph_height * 1.8f && + context.font().texture()->revision > revision, + "2x display requests larger glyph rasterization"); + check(std::abs(second_glyph->width - (second_glyph->uv_rect[2] - second_glyph->uv_rect[0]) * + second_glyph->texture->width) < .1f, + "High-DPI glyphs use one atlas texel per pixel, not stretched 1x bitmaps"); + context.handle(mouse(render::Event::Type::MouseDown, button.rect.x + 300, button.rect.y + 40)); + context.handle(mouse(render::Event::Type::MouseUp, button.rect.x + 300, button.rect.y + 40)); + check(clicks == 1, "Hit testing reaches the second half of a scaled control"); + const auto r = number.rect; + context.handle(mouse(render::Event::Type::MouseDown, r.x + 20, r.y + 20)); + context.handle(mouse(render::Event::Type::MouseMove, r.x + 40, r.y + 20)); + check(number.value == 14, "Numeric drag uses logical distance at 2x"); + context.layout(320, 240, 1); + context.handle(mouse(render::Event::Type::MouseUp, 40, 52)); + check(number.value == 4 && number_commits == 0, + "Monitor scale change cancels pointer drag without an accidental commit"); + check(field.rect.width == initial.width && button.layout.width == 180, + "Returning to 1x preserves logical layout metrics"); + ui::Context scroll(path_from_utf8(FASET_TEST_FONT)); + auto& panel = scroll.root().add(ui::Kind::Column, "scroll"); + panel.layout.height = 80; + panel.layout.scroll = true; + panel.layout.gap = 0; + for (int i = 0; i < 10; ++i) + panel.add(ui::Kind::Button, "row-" + std::to_string(i), "Row").layout.height = 30; + scroll.layout(200, 100, 1); + panel.scroll_y = 30; + scroll.layout(200, 100, 1); + scroll.layout(400, 200, 2); + check(panel.scroll_y == 60 && panel.children[1]->rect.y == panel.rect.y, + "DPI changes retain the logical scroll position"); +} } // namespace int main() { try { + display_scale_contract(); ui::TextBuffer buffer("Привет"); check(buffer.backspace() && buffer.text() == "Приве", "UTF-8 backspace split codepoint"); check(buffer.undo() && buffer.text() == "Привет", "text undo"); @@ -53,7 +132,37 @@ int main() { check(buffer.text() == "верца", "UTF-8 delete"); buffer.undo(); check(buffer.text() == "Дверца", "undo delete"); - ui::Context context(FASET_TEST_FONT); + const auto missing_font = std::filesystem::temp_directory_path() / + path_from_utf8("Недоступный шрифт-" + new_id() + ".ttf"); + bool unicode_font_diagnostic = false; + try { + ui::FontAtlas missing(missing_font); + } catch (const std::exception& error) { + unicode_font_diagnostic = + std::string(error.what()).find(path_to_utf8(missing_font)) != std::string::npos; + } + check(unicode_font_diagnostic, "Font errors preserve the UTF-8 path"); + const auto unicode_font_directory = + std::filesystem::temp_directory_path() / path_from_utf8("Faset шрифты-" + new_id()) / + std::string(80, 'a') / std::string(80, 'b') / std::string(80, 'c'); + std::filesystem::create_directories(unicode_font_directory); + const auto unicode_font_path = + unicode_font_directory / path_from_utf8("Основной шрифт.ttf"); + std::filesystem::copy_file(path_from_utf8(FASET_TEST_FONT), unicode_font_path); + { + ui::FontAtlas unicode_font(unicode_font_path); + check(std::filesystem::remove(unicode_font_path), + "Unicode font file must be closed after loading"); + check(unicode_font.measure("Живая сцена", 18) > 0, + "Shaping uses retained font bytes after Unicode source file is removed"); + render::Snapshot glyphs; + unicode_font.draw(glyphs, "Живая сцена", 0, 0, 18, {1, 1, 1, 1}, {0, 0, 400, 40}); + check(!glyphs.ui_quads.empty() && unicode_font.texture()->revision > 1, + "Rasterization remains valid after Unicode font source disappears"); + } + std::filesystem::remove_all( + unicode_font_directory.parent_path().parent_path().parent_path()); + ui::Context context(path_from_utf8(FASET_TEST_FONT)); auto& root = context.root(); root.layout.gap = 4; auto& name = root.add(ui::Kind::TextField, "name", "Door"); @@ -137,7 +246,7 @@ int main() { context.handle(key("Tab")); check(context.focused_id() == "name", "focus wraps predictably"); check(ime_rectangles > 0, "IME rectangle never sent"); - ui::Context split(FASET_TEST_FONT); + ui::Context split(path_from_utf8(FASET_TEST_FONT)); auto& row = split.root().add(ui::Kind::Row, "row"); row.layout.flex = 1; row.layout.gap = 0; @@ -171,7 +280,7 @@ int main() { context.handle(key("Escape")); check(number.value == 19 && number_commits == 1 && cancellations == 1, "Escape must cancel numeric drag"); - ui::Context scrolling(FASET_TEST_FONT); + ui::Context scrolling(path_from_utf8(FASET_TEST_FONT)); auto& list = scrolling.root().add(ui::Kind::Panel, "list"); list.layout.height = 80; list.layout.scroll = true; @@ -211,7 +320,7 @@ int main() { rejected = true; } check(rejected && restored.to_json() == state, "invalid dock load mutated existing layout"); - ui::Context declarative(FASET_TEST_FONT); + ui::Context declarative(path_from_utf8(FASET_TEST_FONT)); declarative.apply_layout({{"id", "root"}, {"kind", "column"}, {"children", ui::Json::array({{{"id", "run"}, diff --git a/tools/ci/prepare_windows_vulkan.py b/tools/ci/prepare_windows_vulkan.py index 16fe1eb..222b20e 100644 --- a/tools/ci/prepare_windows_vulkan.py +++ b/tools/ci/prepare_windows_vulkan.py @@ -89,12 +89,17 @@ def main() -> int: args = parser.parse_args() if args.cache_key: identity = cache_identity() - key = "windows-2025-vulkan-v2-" + identity + # Cache restoration can prefix-match the primary key. Keep SDK-only and + # full-driver keys in disjoint namespaces, never suffix variants. + key = "windows-2025-vulkan-full-v3-" + identity + sdk_key = "windows-2025-vulkan-sdk-v3-" + identity legacy = LEGACY_CACHE_KEY if identity == LEGACY_SOURCE_IDENTITY else "" + sdk_legacy = ("windows-2025-vulkan-v2-" + identity + "-loader-only" + if identity == LEGACY_SOURCE_IDENTITY else "") print(key) if "GITHUB_OUTPUT" in os.environ: with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: - output.write(f"key={key}\nlegacy={legacy}\n") + output.write(f"key={key}\nsdk_key={sdk_key}\nlegacy={legacy}\nsdk_legacy={sdk_legacy}\n") return 0 if sys.platform != "win32": parser.error("Run this helper on Windows in a Visual Studio x64 developer environment") diff --git a/tools/verify_blender_roundtrip.py b/tools/verify_blender_roundtrip.py index b12a6e4..8c9e817 100644 --- a/tools/verify_blender_roundtrip.py +++ b/tools/verify_blender_roundtrip.py @@ -10,7 +10,7 @@ import tempfile ROOT = pathlib.Path(__file__).resolve().parents[1] -def verify(blender, editor, output): +def verify(blender, editor, output, ui_probe=None): fixture = output / "fixture" result = subprocess.run([str(blender), "--background", "--factory-startup", "--python-exit-code", "1", "--python", str(ROOT / "tests/blender/generate_fixture.py"), "--", str(ROOT), str(fixture)], @@ -64,6 +64,12 @@ def verify(blender, editor, output): "renamed_generation": renamed["result"]["generation"], "stable_output_ids": True, "removed_output_conflict": True, "failed_import_keeps_generation": True, "authoring_preserved": True} + if ui_probe: + probe = subprocess.run([str(ui_probe), str(fixture), str(output / "live-editor")], + capture_output=True, text=True, encoding="utf-8") + (output / "live-editor.log").write_text(probe.stdout + probe.stderr, encoding="utf-8") + probe.check_returncode() + report["live_editor"] = json.loads((output / "live-editor/probe.json").read_text()) (output / "report.json").write_text(json.dumps(report, indent=2) + "\n") print(json.dumps(report, indent=2)) @@ -73,10 +79,14 @@ if __name__ == "__main__": parser.add_argument("--blender", type=pathlib.Path, required=True) parser.add_argument("--editor", type=pathlib.Path, required=True) parser.add_argument("--output", type=pathlib.Path) + parser.add_argument("--ui-probe", type=pathlib.Path, + help="faset_blender_editor_probe: verify two live viewport instances (Vulkan required)") args = parser.parse_args() if args.output: args.output.resolve().mkdir(parents=True, exist_ok=True) - verify(args.blender.resolve(), args.editor.resolve(), args.output.resolve()) + verify(args.blender.resolve(), args.editor.resolve(), args.output.resolve(), + args.ui_probe.resolve() if args.ui_probe else None) else: with tempfile.TemporaryDirectory(prefix="faset-blender-") as temporary: - verify(args.blender.resolve(), args.editor.resolve(), pathlib.Path(temporary)) + verify(args.blender.resolve(), args.editor.resolve(), pathlib.Path(temporary), + args.ui_probe.resolve() if args.ui_probe else None) diff --git a/tools/verify_editor_recovery.py b/tools/verify_editor_recovery.py new file mode 100644 index 0000000..01547a2 --- /dev/null +++ b/tools/verify_editor_recovery.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Exercise crash recovery, failed Player startup and failed C++ build over real MCP. + +Uses a disposable copy of the 2D sample. Requires native build tools and a Vulkan +desktop for the successful Play/Stop check. No runtime entity API is used. +""" +import argparse +import datetime +import hashlib +import json +from pathlib import Path +import platform +import queue +import shutil +import subprocess +import threading +import time + + +class Client: + def __init__(self, editor, engine, project): + self.process = subprocess.Popen([str(editor), "--engine", str(engine), "--project", + str(project), "--mcp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, encoding="utf-8") + self.output, self.errors, self.sequence = queue.Queue(), [], 0 + def output(): + try: + for line in self.process.stdout: + self.output.put(json.loads(line)) + except Exception as error: + self.output.put(error) + finally: + self.output.put(None) + def errors(): + for line in self.process.stderr: + self.errors.append(line) + self.readers = [threading.Thread(target=output, daemon=True), threading.Thread(target=errors, daemon=True)] + for reader in self.readers: + reader.start() + self.request("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, + "clientInfo": {"name": "recovery-acceptance", "version": "1"}}) + self.process.stdin.write('{"jsonrpc":"2.0","method":"notifications/initialized"}\n') + self.process.stdin.flush() + + def request(self, method, params): + self.sequence += 1 + self.process.stdin.write(json.dumps({"jsonrpc": "2.0", "id": self.sequence, + "method": method, "params": params}) + "\n") + self.process.stdin.flush() + response = self.output.get(timeout=30) + assert isinstance(response, dict) and response.get("id") == self.sequence, response + assert "error" not in response, response + return response["result"] + + def call(self, name, arguments=None, error=False): + result = self.request("tools/call", {"name": name, "arguments": arguments or {}}) + assert result.get("isError", False) == error, result + return result["structuredContent"] + + def job(self, identity, expected="succeeded"): + deadline = time.monotonic() + 1800 + while time.monotonic() < deadline: + value = self.call("faset_job", {"id": identity}) + if value["state"] in ("succeeded", "failed", "cancelled", "conflict"): + assert value["state"] == expected, value + return value + time.sleep(.1) + raise RuntimeError("Editor job timed out") + + def close(self, crash=False): + if self.process.poll() is None: + if crash: + self.process.kill() + else: + self.process.stdin.close() + try: + self.process.wait(timeout=30) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait() + raise + for reader in self.readers: + reader.join(timeout=5) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--editor", type=Path, required=True) + parser.add_argument("--engine", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + engine, editor, output = args.engine.resolve(), args.editor.resolve(), args.output.resolve() + if output.exists(): + parser.error("Use a new output directory") + output.mkdir(parents=True) + project = output / "project" + shutil.copytree(engine / "examples/projects/collect-2d", project, + ignore=shutil.ignore_patterns(".faset", "Exports")) + checks = [] + def record(name): + checks.append(name) + (output / "progress.json").write_text(json.dumps(checks, indent=2) + "\n") + print(name, flush=True) + client = Client(editor, engine, project) + try: + doc = client.call("faset_documents")["documents"][0] + identity = doc["id"] + client.call("faset_document_save", {"document": identity}) + before = client.call("faset_document_query", {"document": identity}) + changed = client.call("faset_scene_edit", {"document": identity, "revision": before["revision"], + "operations": [{"op": "scene.rename", "name": "Unsaved recovery acceptance"}]}) + client.close(crash=True) # Kill only our idle owned Editor, after transaction acknowledgement. + client = Client(editor, engine, project) + reopened = client.call("faset_document_query", {"document": identity}) + assert reopened["scene"]["name"] == before["scene"]["name"] + restored = client.call("faset_recovery_restore", {"document": identity, "revision": reopened["revision"]}) + assert restored["scene"] == changed["scene"] + record("acknowledged unsaved edit survives killed Editor and explicit recovery") + (project / "Scenes/blocked.scene.json").mkdir() + client.call("faset_document_save", {"document": identity, "path": "Scenes/blocked.scene.json"}, error=True) + assert client.call("faset_document_query", {"document": identity})["scene"] == restored["scene"] + assert json.loads((project / "Scenes/main.scene.json").read_text())["name"] == before["scene"]["name"] + record("failed save preserves live authoring and last saved file") + bad = client.call("faset_scene_edit", {"document": identity, "revision": restored["revision"], + "operations": [{"op": "entity.create", "entity": { + "id": "future-version-object", "name": "Future component recovery fixture", "parent": None, + "components": [{"id": "future-transform", "type": "faset.transform", "version": 999, + "fields": {"kept": True}}]}}]}) + play = client.call("faset_play", {"document": identity}) + client.job(play["job"]) + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + logs = client.call("faset_editor_logs")["logs"] + if any("Player exited with code 1" in line for line in logs): + break + time.sleep(.05) + else: + raise RuntimeError("Expected failed Player startup was not observed: " + str(logs)) + assert client.call("faset_document_query", {"document": identity})["scene"] == bad["scene"] + record("failed Player process is reported without changing authoring data") + restored = client.call("faset_undo", {"document": identity, "revision": bad["revision"]}) + assert restored["scene"] == changed["scene"] + old_logs = len(client.call("faset_editor_logs")["logs"]) + client.job(client.call("faset_play", {"document": identity})["job"]) + time.sleep(2) + logs = client.call("faset_editor_logs")["logs"][old_logs:] + assert any("Play started" in line for line in logs), logs + assert not any("Player exited with code" in line for line in logs), logs + client.call("faset_stop") + assert client.call("faset_document_query", {"document": identity})["scene"] == restored["scene"] + record("Undo repairs the scene, then Play and Stop leave authoring unchanged") + metadata = client.call("faset_schema") + gameplay = project / "Scripts/Gameplay.cpp" + original = gameplay.read_bytes() + gameplay.write_bytes(original + b"\n#error intentional_recovery_acceptance_compile_failure\n") + try: + client.job(client.call("faset_build")["job"], "failed") + assert client.call("faset_schema") == metadata + assert client.call("faset_schema_status")["stale"] + record("failed C++ build keeps prior schema and reports stale status") + finally: + gameplay.write_bytes(original) + client.job(client.call("faset_build")["job"]) + assert not client.call("faset_schema_status")["stale"] + record("fixed C++ rebuild restores current schema status") + result = client.call("faset_import", {"path": "Assets/missing.glb"}) + client.job(result["job"], "failed") + assert client.call("faset_document_query", {"document": identity})["scene"] == restored["scene"] + record("failed import preserves authoring") + (output / "report.json").write_text(json.dumps({"format": "faset.editor-recovery-verification", + "version": 1, "status": "passed", "checks": checks, + "recorded_at_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "platform": platform.platform(), + "editor_sha256": hashlib.sha256(editor.read_bytes()).hexdigest(), + "logs": client.call("faset_editor_logs")["logs"]}, indent=2) + "\n") + finally: + client.close() + + +if __name__ == "__main__": + main() diff --git a/tools/verify_playable_exports.py b/tools/verify_playable_exports.py index 9a35e6d..4ce1df2 100644 --- a/tools/verify_playable_exports.py +++ b/tools/verify_playable_exports.py @@ -109,6 +109,8 @@ def verify_capture(path: Path) -> dict: def main() -> int: + # Keep captured CI logs portable even when the Windows console uses a legacy code page. + sys.stdout.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--engine", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--editor", type=Path, required=True) @@ -127,9 +129,11 @@ def main() -> int: require(not standalone.exists() or not any(standalone.iterdir()), "Standalone root must be new/empty") standalone.mkdir(parents=True, exist_ok=True) - evidence, projects = output / "evidence", output / "projects" + evidence, projects = output / "evidence", output / "Faset Café 世界" evidence.mkdir() projects.mkdir() + standalone_projects = standalone / "Faset Café 世界" + standalone_projects.mkdir() report = {"format": "faset.playable-export-verification", "version": 1, "started_utc": datetime.now(timezone.utc).isoformat(), "platform": sys.platform, "engine": str(engine), "editor": str(editor), "standalone_root": str(standalone), @@ -162,7 +166,7 @@ def main() -> int: manifest = verify_package(generation) require(dimension != 3 or bool(manifest["asset_generations"]), "3D game did not package its imported Blender asset") - relocated = standalone / name + relocated = standalone_projects / name shutil.copytree(generation, relocated) verify_package(relocated) shutil.copy2(relocated / "manifest.json", evidence / f"{name}-manifest.json")